• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 19769979346

28 Nov 2025 05:05PM UTC coverage: 60.41%. First build
19769979346

Pull #951

github

web-flow
Merge c544fea22 into 7f41af1e2
Pull Request #951: Add facet-assert crate for structural assertions without PartialEq

185 of 342 new or added lines in 2 files covered. (54.09%)

15311 of 25345 relevant lines covered (60.41%)

160.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.36
/facet-assert/src/same.rs
1
//! Structural sameness checking for Facet types.
2

3
use core::fmt;
4
use facet_core::{Def, Facet, Type, UserType};
5
use facet_pretty::PrettyPrinter;
6
use facet_reflect::{HasFields, Peek};
7

8
/// Result of checking if two values are structurally the same.
9
pub enum Sameness {
10
    /// The values are structurally the same.
11
    Same,
12
    /// The values differ - contains a formatted diff.
13
    Different(String),
14
    /// Encountered an opaque type that cannot be compared.
15
    Opaque {
16
        /// The type name of the opaque type.
17
        type_name: &'static str,
18
    },
19
}
20

21
/// Check if two Facet values are structurally the same.
22
///
23
/// This does NOT require `PartialEq` - it walks the structure via reflection.
24
/// Two values are "same" if they have the same structure and values, even if
25
/// they have different type names.
26
///
27
/// Returns [`Sameness::Opaque`] if either value contains an opaque type.
28
pub fn check_same<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
10✔
29
    let left_peek = Peek::new(left);
10✔
30
    let right_peek = Peek::new(right);
10✔
31

32
    let mut differ = Differ::new();
10✔
33
    match differ.check(left_peek, right_peek) {
10✔
34
        CheckResult::Same => Sameness::Same,
8✔
35
        CheckResult::Different => Sameness::Different(differ.into_diff()),
2✔
NEW
36
        CheckResult::Opaque { type_name } => Sameness::Opaque { type_name },
×
37
    }
38
}
10✔
39

40
enum CheckResult {
41
    Same,
42
    Different,
43
    Opaque { type_name: &'static str },
44
}
45

46
struct Differ {
47
    /// Differences found, stored as lines
48
    diffs: Vec<DiffLine>,
49
    /// Current path for context
50
    path: Vec<PathSegment>,
51
}
52

53
enum PathSegment {
54
    Field(&'static str),
55
    Index(usize),
56
    Variant(&'static str),
57
    Key(String),
58
}
59

60
impl fmt::Display for PathSegment {
61
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2✔
62
        match self {
2✔
63
            PathSegment::Field(name) => write!(f, ".{name}"),
1✔
64
            PathSegment::Index(i) => write!(f, "[{i}]"),
1✔
NEW
65
            PathSegment::Variant(name) => write!(f, "::{name}"),
×
NEW
66
            PathSegment::Key(k) => write!(f, "[{k:?}]"),
×
67
        }
68
    }
2✔
69
}
70

71
enum DiffLine {
72
    Changed {
73
        path: String,
74
        left: String,
75
        right: String,
76
    },
77
    OnlyLeft {
78
        path: String,
79
        value: String,
80
    },
81
    OnlyRight {
82
        path: String,
83
        value: String,
84
    },
85
}
86

87
impl Differ {
88
    fn new() -> Self {
11✔
89
        Self {
11✔
90
            diffs: Vec::new(),
11✔
91
            path: Vec::new(),
11✔
92
        }
11✔
93
    }
11✔
94

95
    fn current_path(&self) -> String {
2✔
96
        if self.path.is_empty() {
2✔
NEW
97
            "root".to_string()
×
98
        } else {
99
            let mut s = String::new();
2✔
100
            for seg in &self.path {
4✔
101
                s.push_str(&seg.to_string());
2✔
102
            }
2✔
103
            s
2✔
104
        }
105
    }
2✔
106

107
    fn format_value(peek: Peek<'_, '_>) -> String {
40✔
108
        let printer = PrettyPrinter::default().with_colors(false);
40✔
109
        printer.format_peek(peek).to_string()
40✔
110
    }
40✔
111

112
    fn record_changed(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) {
2✔
113
        self.diffs.push(DiffLine::Changed {
2✔
114
            path: self.current_path(),
2✔
115
            left: Self::format_value(left),
2✔
116
            right: Self::format_value(right),
2✔
117
        });
2✔
118
    }
2✔
119

NEW
120
    fn record_only_left(&mut self, left: Peek<'_, '_>) {
×
NEW
121
        self.diffs.push(DiffLine::OnlyLeft {
×
NEW
122
            path: self.current_path(),
×
NEW
123
            value: Self::format_value(left),
×
NEW
124
        });
×
NEW
125
    }
×
126

NEW
127
    fn record_only_right(&mut self, right: Peek<'_, '_>) {
×
NEW
128
        self.diffs.push(DiffLine::OnlyRight {
×
NEW
129
            path: self.current_path(),
×
NEW
130
            value: Self::format_value(right),
×
NEW
131
        });
×
NEW
132
    }
×
133

134
    fn into_diff(self) -> String {
2✔
135
        use std::fmt::Write;
136

137
        let mut out = String::new();
2✔
138

139
        for diff in self.diffs {
4✔
140
            match diff {
2✔
141
                DiffLine::Changed { path, left, right } => {
2✔
142
                    writeln!(out, "\x1b[1m{path}\x1b[0m:").unwrap();
2✔
143
                    writeln!(out, "  \x1b[31m- {left}\x1b[0m").unwrap();
2✔
144
                    writeln!(out, "  \x1b[32m+ {right}\x1b[0m").unwrap();
2✔
145
                }
2✔
NEW
146
                DiffLine::OnlyLeft { path, value } => {
×
NEW
147
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in left):").unwrap();
×
NEW
148
                    writeln!(out, "  \x1b[31m- {value}\x1b[0m").unwrap();
×
NEW
149
                }
×
NEW
150
                DiffLine::OnlyRight { path, value } => {
×
NEW
151
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in right):").unwrap();
×
NEW
152
                    writeln!(out, "  \x1b[32m+ {value}\x1b[0m").unwrap();
×
NEW
153
                }
×
154
            }
155
        }
156

157
        out
2✔
158
    }
2✔
159

160
    fn check(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
27✔
161
        // Handle Option BEFORE innermost_peek (since Option's try_borrow_inner fails)
162
        if matches!(left.shape().def, Def::Option(_)) && matches!(right.shape().def, Def::Option(_))
27✔
163
        {
164
            return self.check_options(left, right);
2✔
165
        }
25✔
166

167
        // Unwrap transparent wrappers (like NonZero, newtype wrappers)
168
        let left = left.innermost_peek();
25✔
169
        let right = right.innermost_peek();
25✔
170

171
        // Try scalar comparison first (for leaf values like String, i32, etc.)
172
        // Scalars are compared by their formatted representation.
173
        if matches!(left.shape().def, Def::Scalar) && matches!(right.shape().def, Def::Scalar) {
25✔
174
            let left_str = Self::format_value(left);
18✔
175
            let right_str = Self::format_value(right);
18✔
176
            if left_str == right_str {
18✔
177
                return CheckResult::Same;
16✔
178
            } else {
179
                self.record_changed(left, right);
2✔
180
                return CheckResult::Different;
2✔
181
            }
182
        }
7✔
183

184
        // Try to compare structurally based on type/def
185
        // Note: Many types are UserType::Opaque but still have a useful Def (like Vec -> Def::List)
186
        // So we check Def first before giving up on Opaque types.
187

188
        // Handle lists/arrays/slices (Vec is Opaque but has Def::List)
189
        if left.into_list_like().is_ok() && right.into_list_like().is_ok() {
7✔
190
            return self.check_lists(left, right);
2✔
191
        }
5✔
192

193
        // Handle maps
194
        if matches!(left.shape().def, Def::Map(_)) && matches!(right.shape().def, Def::Map(_)) {
5✔
NEW
195
            return self.check_maps(left, right);
×
196
        }
5✔
197

198
        // Handle smart pointers
199
        if matches!(left.shape().def, Def::Pointer(_))
5✔
200
            && matches!(right.shape().def, Def::Pointer(_))
1✔
201
        {
202
            return self.check_pointers(left, right);
1✔
203
        }
4✔
204

205
        // Handle structs
206
        if let (Type::User(UserType::Struct(_)), Type::User(UserType::Struct(_))) =
207
            (left.shape().ty, right.shape().ty)
4✔
208
        {
209
            return self.check_structs(left, right);
4✔
NEW
210
        }
×
211

212
        // Handle enums
213
        if let (Type::User(UserType::Enum(_)), Type::User(UserType::Enum(_))) =
NEW
214
            (left.shape().ty, right.shape().ty)
×
215
        {
NEW
216
            return self.check_enums(left, right);
×
NEW
217
        }
×
218

219
        // At this point, if either is Opaque and we haven't handled it above, fail
NEW
220
        if matches!(left.shape().ty, Type::User(UserType::Opaque)) {
×
NEW
221
            return CheckResult::Opaque {
×
NEW
222
                type_name: left.shape().type_identifier,
×
NEW
223
            };
×
NEW
224
        }
×
NEW
225
        if matches!(right.shape().ty, Type::User(UserType::Opaque)) {
×
NEW
226
            return CheckResult::Opaque {
×
NEW
227
                type_name: right.shape().type_identifier,
×
NEW
228
            };
×
NEW
229
        }
×
230

231
        // Fallback: format and compare
NEW
232
        let left_str = Self::format_value(left);
×
NEW
233
        let right_str = Self::format_value(right);
×
NEW
234
        if left_str == right_str {
×
NEW
235
            CheckResult::Same
×
236
        } else {
NEW
237
            self.record_changed(left, right);
×
NEW
238
            CheckResult::Different
×
239
        }
240
    }
27✔
241

242
    fn check_structs(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
4✔
243
        let left_struct = left.into_struct().unwrap();
4✔
244
        let right_struct = right.into_struct().unwrap();
4✔
245

246
        let mut any_different = false;
4✔
247
        let mut seen_fields = std::collections::HashSet::new();
4✔
248

249
        // Check all fields in left
250
        for (field, left_value) in left_struct.fields() {
8✔
251
            seen_fields.insert(field.name);
8✔
252
            self.path.push(PathSegment::Field(field.name));
8✔
253

254
            if let Ok(right_value) = right_struct.field_by_name(field.name) {
8✔
255
                match self.check(left_value, right_value) {
8✔
256
                    CheckResult::Same => {}
7✔
257
                    CheckResult::Different => any_different = true,
1✔
NEW
258
                    opaque @ CheckResult::Opaque { .. } => {
×
NEW
259
                        self.path.pop();
×
NEW
260
                        return opaque;
×
261
                    }
262
                }
NEW
263
            } else {
×
NEW
264
                // Field only in left
×
NEW
265
                self.record_only_left(left_value);
×
NEW
266
                any_different = true;
×
NEW
267
            }
×
268

269
            self.path.pop();
8✔
270
        }
271

272
        // Check fields only in right
273
        for (field, right_value) in right_struct.fields() {
8✔
274
            if !seen_fields.contains(field.name) {
8✔
NEW
275
                self.path.push(PathSegment::Field(field.name));
×
NEW
276
                self.record_only_right(right_value);
×
NEW
277
                any_different = true;
×
NEW
278
                self.path.pop();
×
279
            }
8✔
280
        }
281

282
        if any_different {
4✔
283
            CheckResult::Different
1✔
284
        } else {
285
            CheckResult::Same
3✔
286
        }
287
    }
4✔
288

NEW
289
    fn check_enums(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
×
NEW
290
        let left_enum = left.into_enum().unwrap();
×
NEW
291
        let right_enum = right.into_enum().unwrap();
×
292

NEW
293
        let left_variant = left_enum.active_variant().unwrap();
×
NEW
294
        let right_variant = right_enum.active_variant().unwrap();
×
295

296
        // Different variants = different
NEW
297
        if left_variant.name != right_variant.name {
×
NEW
298
            self.record_changed(left, right);
×
NEW
299
            return CheckResult::Different;
×
NEW
300
        }
×
301

302
        // Same variant - check fields
NEW
303
        self.path.push(PathSegment::Variant(left_variant.name));
×
304

NEW
305
        let mut any_different = false;
×
NEW
306
        let mut seen_fields = std::collections::HashSet::new();
×
307

NEW
308
        for (field, left_value) in left_enum.fields() {
×
NEW
309
            seen_fields.insert(field.name);
×
NEW
310
            self.path.push(PathSegment::Field(field.name));
×
311

NEW
312
            if let Ok(Some(right_value)) = right_enum.field_by_name(field.name) {
×
NEW
313
                match self.check(left_value, right_value) {
×
NEW
314
                    CheckResult::Same => {}
×
NEW
315
                    CheckResult::Different => any_different = true,
×
NEW
316
                    opaque @ CheckResult::Opaque { .. } => {
×
NEW
317
                        self.path.pop();
×
NEW
318
                        self.path.pop();
×
NEW
319
                        return opaque;
×
320
                    }
321
                }
NEW
322
            } else {
×
NEW
323
                self.record_only_left(left_value);
×
NEW
324
                any_different = true;
×
NEW
325
            }
×
326

NEW
327
            self.path.pop();
×
328
        }
329

NEW
330
        for (field, right_value) in right_enum.fields() {
×
NEW
331
            if !seen_fields.contains(field.name) {
×
NEW
332
                self.path.push(PathSegment::Field(field.name));
×
NEW
333
                self.record_only_right(right_value);
×
NEW
334
                any_different = true;
×
NEW
335
                self.path.pop();
×
NEW
336
            }
×
337
        }
338

NEW
339
        self.path.pop();
×
340

NEW
341
        if any_different {
×
NEW
342
            CheckResult::Different
×
343
        } else {
NEW
344
            CheckResult::Same
×
345
        }
NEW
346
    }
×
347

348
    fn check_options(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
2✔
349
        let left_opt = left.into_option().unwrap();
2✔
350
        let right_opt = right.into_option().unwrap();
2✔
351

352
        match (left_opt.value(), right_opt.value()) {
2✔
353
            (None, None) => CheckResult::Same,
1✔
354
            (Some(l), Some(r)) => self.check(l, r),
1✔
355
            (Some(_), None) | (None, Some(_)) => {
NEW
356
                self.record_changed(left, right);
×
NEW
357
                CheckResult::Different
×
358
            }
359
        }
360
    }
2✔
361

362
    fn check_lists(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
2✔
363
        let left_list = left.into_list_like().unwrap();
2✔
364
        let right_list = right.into_list_like().unwrap();
2✔
365

366
        let left_items: Vec<_> = left_list.iter().collect();
2✔
367
        let right_items: Vec<_> = right_list.iter().collect();
2✔
368

369
        let mut any_different = false;
2✔
370
        let min_len = left_items.len().min(right_items.len());
2✔
371

372
        // Compare common elements
373
        for i in 0..min_len {
6✔
374
            self.path.push(PathSegment::Index(i));
6✔
375

376
            match self.check(left_items[i], right_items[i]) {
6✔
377
                CheckResult::Same => {}
5✔
378
                CheckResult::Different => any_different = true,
1✔
NEW
379
                opaque @ CheckResult::Opaque { .. } => {
×
NEW
380
                    self.path.pop();
×
NEW
381
                    return opaque;
×
382
                }
383
            }
384

385
            self.path.pop();
6✔
386
        }
387

388
        // Elements only in left (removed)
389
        for (i, item) in left_items.iter().enumerate().skip(min_len) {
2✔
NEW
390
            self.path.push(PathSegment::Index(i));
×
NEW
391
            self.record_only_left(*item);
×
NEW
392
            any_different = true;
×
NEW
393
            self.path.pop();
×
NEW
394
        }
×
395

396
        // Elements only in right (added)
397
        for (i, item) in right_items.iter().enumerate().skip(min_len) {
2✔
NEW
398
            self.path.push(PathSegment::Index(i));
×
NEW
399
            self.record_only_right(*item);
×
NEW
400
            any_different = true;
×
NEW
401
            self.path.pop();
×
NEW
402
        }
×
403

404
        if any_different {
2✔
405
            CheckResult::Different
1✔
406
        } else {
407
            CheckResult::Same
1✔
408
        }
409
    }
2✔
410

NEW
411
    fn check_maps(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
×
NEW
412
        let left_map = left.into_map().unwrap();
×
NEW
413
        let right_map = right.into_map().unwrap();
×
414

NEW
415
        let mut any_different = false;
×
NEW
416
        let mut seen_keys = std::collections::HashSet::new();
×
417

NEW
418
        for (left_key, left_value) in left_map.iter() {
×
NEW
419
            let key_str = Self::format_value(left_key);
×
NEW
420
            seen_keys.insert(key_str.clone());
×
NEW
421
            self.path.push(PathSegment::Key(key_str));
×
422

423
            // Try to find matching key in right map
NEW
424
            let mut found = false;
×
NEW
425
            for (right_key, right_value) in right_map.iter() {
×
NEW
426
                if Self::format_value(left_key) == Self::format_value(right_key) {
×
NEW
427
                    found = true;
×
NEW
428
                    match self.check(left_value, right_value) {
×
NEW
429
                        CheckResult::Same => {}
×
NEW
430
                        CheckResult::Different => any_different = true,
×
NEW
431
                        opaque @ CheckResult::Opaque { .. } => {
×
NEW
432
                            self.path.pop();
×
NEW
433
                            return opaque;
×
434
                        }
435
                    }
NEW
436
                    break;
×
NEW
437
                }
×
438
            }
439

NEW
440
            if !found {
×
NEW
441
                self.record_only_left(left_value);
×
NEW
442
                any_different = true;
×
NEW
443
            }
×
444

NEW
445
            self.path.pop();
×
446
        }
447

448
        // Check keys only in right
NEW
449
        for (right_key, right_value) in right_map.iter() {
×
NEW
450
            let key_str = Self::format_value(right_key);
×
NEW
451
            if !seen_keys.contains(&key_str) {
×
NEW
452
                self.path.push(PathSegment::Key(key_str));
×
NEW
453
                self.record_only_right(right_value);
×
NEW
454
                any_different = true;
×
NEW
455
                self.path.pop();
×
NEW
456
            }
×
457
        }
458

NEW
459
        if any_different {
×
NEW
460
            CheckResult::Different
×
461
        } else {
NEW
462
            CheckResult::Same
×
463
        }
NEW
464
    }
×
465

466
    fn check_pointers(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
1✔
467
        let left_ptr = left.into_pointer().unwrap();
1✔
468
        let right_ptr = right.into_pointer().unwrap();
1✔
469

470
        match (left_ptr.borrow_inner(), right_ptr.borrow_inner()) {
1✔
471
            (Some(left_inner), Some(right_inner)) => self.check(left_inner, right_inner),
1✔
NEW
472
            (None, None) => CheckResult::Same,
×
473
            _ => {
NEW
474
                self.record_changed(left, right);
×
NEW
475
                CheckResult::Different
×
476
            }
477
        }
478
    }
1✔
479
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc