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

facet-rs / facet / 20077181072

09 Dec 2025 08:15PM UTC coverage: 58.588% (+0.09%) from 58.494%
20077181072

push

github

fasterthanlime
fix: add rust-version to cinereus and update snapshot

27084 of 46228 relevant lines covered (58.59%)

615.2 hits per line

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

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

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

9
/// Options for customizing structural comparison behavior.
10
///
11
/// Use the builder pattern to configure options:
12
///
13
/// ```
14
/// use facet_assert::SameOptions;
15
///
16
/// let options = SameOptions::new()
17
///     .float_tolerance(1e-6);
18
/// ```
19
#[derive(Debug, Clone, Default)]
20
pub struct SameOptions {
21
    /// Tolerance for floating-point comparisons.
22
    /// If set, two floats are considered equal if their absolute difference
23
    /// is less than or equal to this value.
24
    float_tolerance: Option<f64>,
25
}
26

27
impl SameOptions {
28
    /// Create a new `SameOptions` with default settings (exact comparison).
29
    pub fn new() -> Self {
10✔
30
        Self::default()
10✔
31
    }
10✔
32

33
    /// Set the tolerance for floating-point comparisons.
34
    ///
35
    /// When set, two `f32` or `f64` values are considered equal if:
36
    /// `|left - right| <= tolerance`
37
    ///
38
    /// # Example
39
    ///
40
    /// ```
41
    /// use facet_assert::{assert_same_with, SameOptions};
42
    ///
43
    /// let a = 1.0000001_f64;
44
    /// let b = 1.0000002_f64;
45
    ///
46
    /// // This would fail with exact comparison:
47
    /// // assert_same!(a, b);
48
    ///
49
    /// // But passes with tolerance:
50
    /// assert_same_with!(a, b, SameOptions::new().float_tolerance(1e-6));
51
    /// ```
52
    pub fn float_tolerance(mut self, tolerance: f64) -> Self {
10✔
53
        self.float_tolerance = Some(tolerance);
10✔
54
        self
10✔
55
    }
10✔
56
}
57

58
/// Result of checking if two values are structurally the same.
59
pub enum Sameness {
60
    /// The values are structurally the same.
61
    Same,
62
    /// The values differ - contains a formatted diff.
63
    Different(String),
64
    /// Encountered an opaque type that cannot be compared.
65
    Opaque {
66
        /// The type name of the opaque type.
67
        type_name: &'static str,
68
    },
69
}
70

71
/// Check if two Facet values are structurally the same.
72
///
73
/// This does NOT require `PartialEq` - it walks the structure via reflection.
74
/// Two values are "same" if they have the same structure and values, even if
75
/// they have different type names.
76
///
77
/// Returns [`Sameness::Opaque`] if either value contains an opaque type.
78
pub fn check_same<'f, T: Facet<'f>, U: Facet<'f>>(left: &T, right: &U) -> Sameness {
33✔
79
    check_same_with(left, right, SameOptions::default())
33✔
80
}
33✔
81

82
/// Check if two Facet values are structurally the same, with custom options.
83
///
84
/// Like [`check_same`], but allows configuring comparison behavior via [`SameOptions`].
85
///
86
/// # Example
87
///
88
/// ```
89
/// use facet_assert::{check_same_with, SameOptions, Sameness};
90
///
91
/// let a = 1.0000001_f64;
92
/// let b = 1.0000002_f64;
93
///
94
/// // With tolerance, these are considered the same
95
/// let options = SameOptions::new().float_tolerance(1e-6);
96
/// assert!(matches!(check_same_with(&a, &b, options), Sameness::Same));
97
/// ```
98
pub fn check_same_with<'f, T: Facet<'f>, U: Facet<'f>>(
39✔
99
    left: &T,
39✔
100
    right: &U,
39✔
101
    options: SameOptions,
39✔
102
) -> Sameness {
39✔
103
    let left_peek = Peek::new(left);
39✔
104
    let right_peek = Peek::new(right);
39✔
105

106
    let mut differ = Differ::new(options);
39✔
107
    match differ.check(left_peek, right_peek) {
39✔
108
        CheckResult::Same => Sameness::Same,
26✔
109
        CheckResult::Different => Sameness::Different(differ.into_diff()),
13✔
110
        CheckResult::Opaque { type_name } => Sameness::Opaque { type_name },
×
111
    }
112
}
39✔
113

114
enum CheckResult {
115
    Same,
116
    Different,
117
    Opaque { type_name: &'static str },
118
}
119

120
struct Differ {
121
    /// Differences found, stored as lines
122
    diffs: Vec<DiffLine>,
123
    /// Current path for context
124
    path: Vec<PathSegment>,
125
    /// Comparison options
126
    options: SameOptions,
127
}
128

129
enum PathSegment {
130
    Field(&'static str),
131
    Index(usize),
132
    Variant(&'static str),
133
    Key(String),
134
}
135

136
impl fmt::Display for PathSegment {
137
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10✔
138
        match self {
10✔
139
            PathSegment::Field(name) => write!(f, ".{name}"),
2✔
140
            PathSegment::Index(i) => write!(f, "[{i}]"),
7✔
141
            PathSegment::Variant(name) => write!(f, "::{name}"),
×
142
            PathSegment::Key(k) => write!(f, "[{k:?}]"),
1✔
143
        }
144
    }
10✔
145
}
146

147
enum DiffLine {
148
    Changed {
149
        path: String,
150
        left: String,
151
        right: String,
152
    },
153
    OnlyLeft {
154
        path: String,
155
        value: String,
156
    },
157
    OnlyRight {
158
        path: String,
159
        value: String,
160
    },
161
}
162

163
impl Differ {
164
    fn new(options: SameOptions) -> Self {
43✔
165
        Self {
43✔
166
            diffs: Vec::new(),
43✔
167
            path: Vec::new(),
43✔
168
            options,
43✔
169
        }
43✔
170
    }
43✔
171

172
    /// Compare two f64 values, using tolerance if configured.
173
    fn floats_equal(&self, left: f64, right: f64) -> bool {
14✔
174
        if let Some(tolerance) = self.options.float_tolerance {
14✔
175
            (left - right).abs() <= tolerance
14✔
176
        } else {
177
            left == right
×
178
        }
179
    }
14✔
180

181
    /// Try to extract f64 values from two Peek values if they are both floats.
182
    /// Returns None if either value is not a float type.
183
    fn extract_floats(&self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> Option<(f64, f64)> {
15✔
184
        let left_f64 = match left.scalar_type()? {
15✔
185
            ScalarType::F64 => *left.get::<f64>().ok()?,
13✔
186
            ScalarType::F32 => *left.get::<f32>().ok()? as f64,
1✔
187
            _ => return None,
1✔
188
        };
189
        let right_f64 = match right.scalar_type()? {
14✔
190
            ScalarType::F64 => *right.get::<f64>().ok()?,
13✔
191
            ScalarType::F32 => *right.get::<f32>().ok()? as f64,
1✔
192
            _ => return None,
×
193
        };
194
        Some((left_f64, right_f64))
14✔
195
    }
15✔
196

197
    fn current_path(&self) -> String {
15✔
198
        if self.path.is_empty() {
15✔
199
            "root".to_string()
5✔
200
        } else {
201
            let mut s = String::new();
10✔
202
            for seg in &self.path {
10✔
203
                s.push_str(&seg.to_string());
10✔
204
            }
10✔
205
            s
10✔
206
        }
207
    }
15✔
208

209
    fn format_value(peek: Peek<'_, '_>) -> String {
969✔
210
        let printer = PrettyPrinter::default()
969✔
211
            .with_colors(false)
969✔
212
            .with_minimal_option_names(true);
969✔
213
        printer.format_peek(peek).to_string()
969✔
214
    }
969✔
215

216
    fn record_changed(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) {
14✔
217
        self.diffs.push(DiffLine::Changed {
14✔
218
            path: self.current_path(),
14✔
219
            left: Self::format_value(left),
14✔
220
            right: Self::format_value(right),
14✔
221
        });
14✔
222
    }
14✔
223

224
    fn record_only_left(&mut self, left: Peek<'_, '_>) {
1✔
225
        self.diffs.push(DiffLine::OnlyLeft {
1✔
226
            path: self.current_path(),
1✔
227
            value: Self::format_value(left),
1✔
228
        });
1✔
229
    }
1✔
230

231
    fn record_only_right(&mut self, right: Peek<'_, '_>) {
×
232
        self.diffs.push(DiffLine::OnlyRight {
×
233
            path: self.current_path(),
×
234
            value: Self::format_value(right),
×
235
        });
×
236
    }
×
237

238
    fn into_diff(self) -> String {
13✔
239
        use std::fmt::Write;
240

241
        let mut out = String::new();
13✔
242

243
        for diff in self.diffs {
15✔
244
            match diff {
15✔
245
                DiffLine::Changed { path, left, right } => {
14✔
246
                    writeln!(out, "\x1b[1m{path}\x1b[0m:").unwrap();
14✔
247
                    writeln!(out, "  \x1b[31m- {left}\x1b[0m").unwrap();
14✔
248
                    writeln!(out, "  \x1b[32m+ {right}\x1b[0m").unwrap();
14✔
249

250
                    // Check if the strings are confusable (look identical but differ)
251
                    // Use the confusables crate for detection, then show character-level diff
252
                    let left_normalized = left.replace_confusable();
14✔
253
                    let right_normalized = right.replace_confusable();
14✔
254
                    if left_normalized == right_normalized
14✔
255
                        && let Some(explanation) = explain_confusable_differences(&left, &right)
×
256
                    {
×
257
                        writeln!(out, "  \x1b[33m{}\x1b[0m", explanation).unwrap();
×
258
                    }
14✔
259
                }
260
                DiffLine::OnlyLeft { path, value } => {
1✔
261
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in left):").unwrap();
1✔
262
                    writeln!(out, "  \x1b[31m- {value}\x1b[0m").unwrap();
1✔
263
                }
1✔
264
                DiffLine::OnlyRight { path, value } => {
×
265
                    writeln!(out, "\x1b[1m{path}\x1b[0m (only in right):").unwrap();
×
266
                    writeln!(out, "  \x1b[32m+ {value}\x1b[0m").unwrap();
×
267
                }
×
268
            }
269
        }
270

271
        out
13✔
272
    }
13✔
273

274
    fn check(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
2,259✔
275
        // Handle Option BEFORE innermost_peek (since Option's try_borrow_inner fails)
276
        if matches!(left.shape().def, Def::Option(_)) && matches!(right.shape().def, Def::Option(_))
2,259✔
277
        {
278
            return self.check_options(left, right);
289✔
279
        }
1,970✔
280

281
        // Unwrap transparent wrappers (like NonZero, newtype wrappers)
282
        let left = left.innermost_peek();
1,970✔
283
        let right = right.innermost_peek();
1,970✔
284

285
        // Try scalar comparison first (for leaf values like String, i32, etc.)
286
        // Scalars are compared by their formatted representation, except for floats
287
        // with tolerance configured.
288
        if matches!(left.shape().def, Def::Scalar) && matches!(right.shape().def, Def::Scalar) {
1,970✔
289
            // Try float comparison with tolerance if configured
290
            if self.options.float_tolerance.is_some()
474✔
291
                && let Some((left_f64, right_f64)) = self.extract_floats(left, right)
15✔
292
            {
293
                if self.floats_equal(left_f64, right_f64) {
14✔
294
                    return CheckResult::Same;
13✔
295
                } else {
296
                    self.record_changed(left, right);
1✔
297
                    return CheckResult::Different;
1✔
298
                }
299
            }
460✔
300

301
            // Default: compare by formatted representation
302
            let left_str = Self::format_value(left);
460✔
303
            let right_str = Self::format_value(right);
460✔
304
            if left_str == right_str {
460✔
305
                return CheckResult::Same;
453✔
306
            } else {
307
                self.record_changed(left, right);
7✔
308
                return CheckResult::Different;
7✔
309
            }
310
        }
1,496✔
311

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

316
        // Handle lists/arrays/slices (Vec is Opaque but has Def::List)
317
        if left.into_list_like().is_ok() && right.into_list_like().is_ok() {
1,496✔
318
            return self.check_lists(left, right);
441✔
319
        }
1,055✔
320

321
        // Handle maps
322
        if matches!(left.shape().def, Def::Map(_)) && matches!(right.shape().def, Def::Map(_)) {
1,055✔
323
            return self.check_maps(left, right);
×
324
        }
1,055✔
325

326
        // Handle smart pointers
327
        if matches!(left.shape().def, Def::Pointer(_))
1,055✔
328
            && matches!(right.shape().def, Def::Pointer(_))
101✔
329
        {
330
            return self.check_pointers(left, right);
101✔
331
        }
954✔
332

333
        // Handle structs
334
        if let (Type::User(UserType::Struct(_)), Type::User(UserType::Struct(_))) =
335
            (left.shape().ty, right.shape().ty)
954✔
336
        {
337
            return self.check_structs(left, right);
354✔
338
        }
600✔
339

340
        // Handle enums
341
        if let (Type::User(UserType::Enum(_)), Type::User(UserType::Enum(_))) =
342
            (left.shape().ty, right.shape().ty)
600✔
343
        {
344
            return self.check_enums(left, right);
557✔
345
        }
43✔
346

347
        // Handle dynamic values (like facet_value::Value) - compare based on their runtime kind
348
        // This allows comparing Value against concrete types (e.g., Value array vs Vec)
349
        if let Def::DynamicValue(_) = left.shape().def {
43✔
350
            return self.check_with_dynamic_value(left, right);
42✔
351
        }
1✔
352
        if let Def::DynamicValue(_) = right.shape().def {
1✔
353
            return self.check_with_dynamic_value(right, left);
1✔
354
        }
×
355

356
        // At this point, if either is Opaque and we haven't handled it above, fail
357
        if matches!(left.shape().ty, Type::User(UserType::Opaque)) {
×
358
            return CheckResult::Opaque {
×
359
                type_name: left.shape().type_identifier,
×
360
            };
×
361
        }
×
362
        if matches!(right.shape().ty, Type::User(UserType::Opaque)) {
×
363
            return CheckResult::Opaque {
×
364
                type_name: right.shape().type_identifier,
×
365
            };
×
366
        }
×
367

368
        // Fallback: format and compare
369
        let left_str = Self::format_value(left);
×
370
        let right_str = Self::format_value(right);
×
371
        if left_str == right_str {
×
372
            CheckResult::Same
×
373
        } else {
374
            self.record_changed(left, right);
×
375
            CheckResult::Different
×
376
        }
377
    }
2,259✔
378

379
    fn check_structs(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
354✔
380
        let left_struct = left.into_struct().unwrap();
354✔
381
        let right_struct = right.into_struct().unwrap();
354✔
382

383
        let mut any_different = false;
354✔
384
        let mut seen_fields = std::collections::HashSet::new();
354✔
385

386
        // Check all fields in left
387
        for (field, left_value) in left_struct.fields() {
1,653✔
388
            seen_fields.insert(field.name);
1,653✔
389
            self.path.push(PathSegment::Field(field.name));
1,653✔
390

391
            if let Ok(right_value) = right_struct.field_by_name(field.name) {
1,653✔
392
                match self.check(left_value, right_value) {
1,653✔
393
                    CheckResult::Same => {}
1,651✔
394
                    CheckResult::Different => any_different = true,
2✔
395
                    opaque @ CheckResult::Opaque { .. } => {
×
396
                        self.path.pop();
×
397
                        return opaque;
×
398
                    }
399
                }
400
            } else {
×
401
                // Field only in left
×
402
                self.record_only_left(left_value);
×
403
                any_different = true;
×
404
            }
×
405

406
            self.path.pop();
1,653✔
407
        }
408

409
        // Check fields only in right
410
        for (field, right_value) in right_struct.fields() {
1,653✔
411
            if !seen_fields.contains(field.name) {
1,653✔
412
                self.path.push(PathSegment::Field(field.name));
×
413
                self.record_only_right(right_value);
×
414
                any_different = true;
×
415
                self.path.pop();
×
416
            }
1,653✔
417
        }
418

419
        if any_different {
354✔
420
            CheckResult::Different
2✔
421
        } else {
422
            CheckResult::Same
352✔
423
        }
424
    }
354✔
425

426
    fn check_enums(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
557✔
427
        let left_enum = left.into_enum().unwrap();
557✔
428
        let right_enum = right.into_enum().unwrap();
557✔
429

430
        let left_variant = left_enum.active_variant().unwrap();
557✔
431
        let right_variant = right_enum.active_variant().unwrap();
557✔
432

433
        // Different variants = different
434
        if left_variant.name != right_variant.name {
557✔
435
            self.record_changed(left, right);
×
436
            return CheckResult::Different;
×
437
        }
557✔
438

439
        // Same variant - check fields
440
        self.path.push(PathSegment::Variant(left_variant.name));
557✔
441

442
        let mut any_different = false;
557✔
443
        let mut seen_fields = std::collections::HashSet::new();
557✔
444

445
        for (field, left_value) in left_enum.fields() {
557✔
446
            seen_fields.insert(field.name);
271✔
447
            self.path.push(PathSegment::Field(field.name));
271✔
448

449
            if let Ok(Some(right_value)) = right_enum.field_by_name(field.name) {
271✔
450
                match self.check(left_value, right_value) {
271✔
451
                    CheckResult::Same => {}
271✔
452
                    CheckResult::Different => any_different = true,
×
453
                    opaque @ CheckResult::Opaque { .. } => {
×
454
                        self.path.pop();
×
455
                        self.path.pop();
×
456
                        return opaque;
×
457
                    }
458
                }
459
            } else {
×
460
                self.record_only_left(left_value);
×
461
                any_different = true;
×
462
            }
×
463

464
            self.path.pop();
271✔
465
        }
466

467
        for (field, right_value) in right_enum.fields() {
557✔
468
            if !seen_fields.contains(field.name) {
271✔
469
                self.path.push(PathSegment::Field(field.name));
×
470
                self.record_only_right(right_value);
×
471
                any_different = true;
×
472
                self.path.pop();
×
473
            }
271✔
474
        }
475

476
        self.path.pop();
557✔
477

478
        if any_different {
557✔
479
            CheckResult::Different
×
480
        } else {
481
            CheckResult::Same
557✔
482
        }
483
    }
557✔
484

485
    fn check_options(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
289✔
486
        let left_opt = left.into_option().unwrap();
289✔
487
        let right_opt = right.into_option().unwrap();
289✔
488

489
        match (left_opt.value(), right_opt.value()) {
289✔
490
            (None, None) => CheckResult::Same,
216✔
491
            (Some(l), Some(r)) => self.check(l, r),
73✔
492
            (Some(_), None) | (None, Some(_)) => {
493
                self.record_changed(left, right);
×
494
                CheckResult::Different
×
495
            }
496
        }
497
    }
289✔
498

499
    fn check_lists(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
441✔
500
        let left_list = left.into_list_like().unwrap();
441✔
501
        let right_list = right.into_list_like().unwrap();
441✔
502

503
        let left_items: Vec<_> = left_list.iter().collect();
441✔
504
        let right_items: Vec<_> = right_list.iter().collect();
441✔
505

506
        let mut any_different = false;
441✔
507
        let min_len = left_items.len().min(right_items.len());
441✔
508

509
        // Compare common elements
510
        for i in 0..min_len {
441✔
511
            self.path.push(PathSegment::Index(i));
90✔
512

513
            match self.check(left_items[i], right_items[i]) {
90✔
514
                CheckResult::Same => {}
86✔
515
                CheckResult::Different => any_different = true,
4✔
516
                opaque @ CheckResult::Opaque { .. } => {
×
517
                    self.path.pop();
×
518
                    return opaque;
×
519
                }
520
            }
521

522
            self.path.pop();
90✔
523
        }
524

525
        // Elements only in left (removed)
526
        for (i, item) in left_items.iter().enumerate().skip(min_len) {
441✔
527
            self.path.push(PathSegment::Index(i));
×
528
            self.record_only_left(*item);
×
529
            any_different = true;
×
530
            self.path.pop();
×
531
        }
×
532

533
        // Elements only in right (added)
534
        for (i, item) in right_items.iter().enumerate().skip(min_len) {
441✔
535
            self.path.push(PathSegment::Index(i));
×
536
            self.record_only_right(*item);
×
537
            any_different = true;
×
538
            self.path.pop();
×
539
        }
×
540

541
        if any_different {
441✔
542
            CheckResult::Different
2✔
543
        } else {
544
            CheckResult::Same
439✔
545
        }
546
    }
441✔
547

548
    fn check_maps(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
×
549
        let left_map = left.into_map().unwrap();
×
550
        let right_map = right.into_map().unwrap();
×
551

552
        let mut any_different = false;
×
553
        let mut seen_keys = std::collections::HashSet::new();
×
554

555
        for (left_key, left_value) in left_map.iter() {
×
556
            let key_str = Self::format_value(left_key);
×
557
            seen_keys.insert(key_str.clone());
×
558
            self.path.push(PathSegment::Key(key_str));
×
559

560
            // Try to find matching key in right map
561
            let mut found = false;
×
562
            for (right_key, right_value) in right_map.iter() {
×
563
                if Self::format_value(left_key) == Self::format_value(right_key) {
×
564
                    found = true;
×
565
                    match self.check(left_value, right_value) {
×
566
                        CheckResult::Same => {}
×
567
                        CheckResult::Different => any_different = true,
×
568
                        opaque @ CheckResult::Opaque { .. } => {
×
569
                            self.path.pop();
×
570
                            return opaque;
×
571
                        }
572
                    }
573
                    break;
×
574
                }
×
575
            }
576

577
            if !found {
×
578
                self.record_only_left(left_value);
×
579
                any_different = true;
×
580
            }
×
581

582
            self.path.pop();
×
583
        }
584

585
        // Check keys only in right
586
        for (right_key, right_value) in right_map.iter() {
×
587
            let key_str = Self::format_value(right_key);
×
588
            if !seen_keys.contains(&key_str) {
×
589
                self.path.push(PathSegment::Key(key_str));
×
590
                self.record_only_right(right_value);
×
591
                any_different = true;
×
592
                self.path.pop();
×
593
            }
×
594
        }
595

596
        if any_different {
×
597
            CheckResult::Different
×
598
        } else {
599
            CheckResult::Same
×
600
        }
601
    }
×
602

603
    fn check_pointers(&mut self, left: Peek<'_, '_>, right: Peek<'_, '_>) -> CheckResult {
101✔
604
        let left_ptr = left.into_pointer().unwrap();
101✔
605
        let right_ptr = right.into_pointer().unwrap();
101✔
606

607
        match (left_ptr.borrow_inner(), right_ptr.borrow_inner()) {
101✔
608
            (Some(left_inner), Some(right_inner)) => self.check(left_inner, right_inner),
101✔
609
            (None, None) => CheckResult::Same,
×
610
            _ => {
611
                self.record_changed(left, right);
×
612
                CheckResult::Different
×
613
            }
614
        }
615
    }
101✔
616

617
    /// Compare a DynamicValue (left) against any other Peek (right) based on the DynamicValue's runtime kind.
618
    /// This enables comparing e.g. `Value::Array` against `Vec<i32>`.
619
    fn check_with_dynamic_value(
43✔
620
        &mut self,
43✔
621
        dyn_peek: Peek<'_, '_>,
43✔
622
        other: Peek<'_, '_>,
43✔
623
    ) -> CheckResult {
43✔
624
        let dyn_val = dyn_peek.into_dynamic_value().unwrap();
43✔
625
        let kind = dyn_val.kind();
43✔
626

627
        match kind {
43✔
628
            DynValueKind::Null => {
629
                // Null compares equal to () or Option::None
630
                let other_str = Self::format_value(other);
×
631
                if other_str == "()" || other_str == "None" {
×
632
                    CheckResult::Same
×
633
                } else {
634
                    self.record_changed(dyn_peek, other);
×
635
                    CheckResult::Different
×
636
                }
637
            }
638
            DynValueKind::Bool => {
639
                // Compare against bool
640
                let dyn_bool = dyn_val.as_bool();
2✔
641

642
                // Check if other is also a DynamicValue bool
643
                let other_bool = if let Ok(other_dyn) = other.into_dynamic_value() {
2✔
644
                    other_dyn.as_bool()
×
645
                } else {
646
                    let other_str = Self::format_value(other);
2✔
647
                    match other_str.as_str() {
2✔
648
                        "true" => Some(true),
2✔
649
                        "false" => Some(false),
1✔
650
                        _ => None,
×
651
                    }
652
                };
653

654
                if dyn_bool == other_bool {
2✔
655
                    CheckResult::Same
1✔
656
                } else {
657
                    self.record_changed(dyn_peek, other);
1✔
658
                    CheckResult::Different
1✔
659
                }
660
            }
661
            DynValueKind::Number => {
662
                // Check if other is also a DynamicValue number
663
                if let Ok(other_dyn) = other.into_dynamic_value() {
26✔
664
                    // Compare DynamicValue numbers directly
665
                    let same = match (dyn_val.as_i64(), other_dyn.as_i64()) {
8✔
666
                        (Some(l), Some(r)) => l == r,
8✔
667
                        _ => match (dyn_val.as_u64(), other_dyn.as_u64()) {
×
668
                            (Some(l), Some(r)) => l == r,
×
669
                            _ => match (dyn_val.as_f64(), other_dyn.as_f64()) {
×
670
                                (Some(l), Some(r)) => self.floats_equal(l, r),
×
671
                                _ => false,
×
672
                            },
673
                        },
674
                    };
675
                    if same {
8✔
676
                        return CheckResult::Same;
7✔
677
                    } else {
678
                        self.record_changed(dyn_peek, other);
1✔
679
                        return CheckResult::Different;
1✔
680
                    }
681
                }
18✔
682

683
                // Compare against scalar number by parsing formatted value
684
                let other_str = Self::format_value(other);
18✔
685

686
                let same = if let Some(dyn_i64) = dyn_val.as_i64() {
18✔
687
                    other_str.parse::<i64>().ok() == Some(dyn_i64)
18✔
688
                } else if let Some(dyn_u64) = dyn_val.as_u64() {
×
689
                    other_str.parse::<u64>().ok() == Some(dyn_u64)
×
690
                } else if let Some(dyn_f64) = dyn_val.as_f64() {
×
691
                    other_str
×
692
                        .parse::<f64>()
×
693
                        .ok()
×
694
                        .is_some_and(|other_f64| self.floats_equal(dyn_f64, other_f64))
×
695
                } else {
696
                    false
×
697
                };
698

699
                if same {
18✔
700
                    CheckResult::Same
16✔
701
                } else {
702
                    self.record_changed(dyn_peek, other);
2✔
703
                    CheckResult::Different
2✔
704
                }
705
            }
706
            DynValueKind::String => {
707
                // Compare against string types
708
                let dyn_str = dyn_val.as_str();
4✔
709

710
                // Check if other is also a DynamicValue string
711
                let other_str = if let Ok(other_dyn) = other.into_dynamic_value() {
4✔
712
                    other_dyn.as_str()
2✔
713
                } else {
714
                    other.as_str()
2✔
715
                };
716

717
                if dyn_str == other_str {
4✔
718
                    CheckResult::Same
2✔
719
                } else {
720
                    self.record_changed(dyn_peek, other);
2✔
721
                    CheckResult::Different
2✔
722
                }
723
            }
724
            DynValueKind::Bytes => {
725
                // Compare against byte slice types
726
                let dyn_bytes = dyn_val.as_bytes();
×
727

728
                // Check if other is also a DynamicValue bytes
729
                let other_bytes = if let Ok(other_dyn) = other.into_dynamic_value() {
×
730
                    other_dyn.as_bytes()
×
731
                } else {
732
                    other.as_bytes()
×
733
                };
734

735
                if dyn_bytes == other_bytes {
×
736
                    CheckResult::Same
×
737
                } else {
738
                    self.record_changed(dyn_peek, other);
×
739
                    CheckResult::Different
×
740
                }
741
            }
742
            DynValueKind::Array => {
743
                // Compare against any list-like type (Vec, array, slice, or another DynamicValue array)
744
                self.check_dyn_array_against_other(dyn_peek, dyn_val, other)
9✔
745
            }
746
            DynValueKind::Object => {
747
                // Compare against maps or structs
748
                self.check_dyn_object_against_other(dyn_peek, dyn_val, other)
2✔
749
            }
750
            DynValueKind::DateTime => {
751
                // Compare datetime values by their components
752
                let dyn_dt = dyn_val.as_datetime();
×
753

754
                // Check if other is also a DynamicValue datetime
755
                let other_dt = if let Ok(other_dyn) = other.into_dynamic_value() {
×
756
                    other_dyn.as_datetime()
×
757
                } else {
758
                    None
×
759
                };
760

761
                if dyn_dt == other_dt {
×
762
                    CheckResult::Same
×
763
                } else {
764
                    self.record_changed(dyn_peek, other);
×
765
                    CheckResult::Different
×
766
                }
767
            }
768
            DynValueKind::QName | DynValueKind::Uuid => {
769
                // For now, QName and Uuid are compared by formatted representation
770
                let dyn_str = Self::format_value(dyn_peek);
×
771
                let other_str = Self::format_value(other);
×
772
                if dyn_str == other_str {
×
773
                    CheckResult::Same
×
774
                } else {
775
                    self.record_changed(dyn_peek, other);
×
776
                    CheckResult::Different
×
777
                }
778
            }
779
        }
780
    }
43✔
781

782
    fn check_dyn_array_against_other(
9✔
783
        &mut self,
9✔
784
        dyn_peek: Peek<'_, '_>,
9✔
785
        dyn_val: facet_reflect::PeekDynamicValue<'_, '_>,
9✔
786
        other: Peek<'_, '_>,
9✔
787
    ) -> CheckResult {
9✔
788
        let dyn_len = dyn_val.array_len().unwrap_or(0);
9✔
789

790
        // Check if other is also a DynamicValue array
791
        if let Ok(other_dyn) = other.into_dynamic_value() {
9✔
792
            if other_dyn.kind() == DynValueKind::Array {
2✔
793
                let other_len = other_dyn.array_len().unwrap_or(0);
2✔
794
                return self
2✔
795
                    .check_two_dyn_arrays(dyn_peek, dyn_val, dyn_len, other, other_dyn, other_len);
2✔
796
            } else {
797
                self.record_changed(dyn_peek, other);
×
798
                return CheckResult::Different;
×
799
            }
800
        }
7✔
801

802
        // Check if other is list-like
803
        if let Ok(other_list) = other.into_list_like() {
7✔
804
            let other_len = other_list.len();
7✔
805
            let mut any_different = false;
7✔
806
            let min_len = dyn_len.min(other_len);
7✔
807

808
            // Compare common elements
809
            for i in 0..min_len {
18✔
810
                self.path.push(PathSegment::Index(i));
18✔
811

812
                if let (Some(dyn_elem), Some(other_elem)) =
18✔
813
                    (dyn_val.array_get(i), other_list.get(i))
18✔
814
                {
815
                    match self.check(dyn_elem, other_elem) {
18✔
816
                        CheckResult::Same => {}
17✔
817
                        CheckResult::Different => any_different = true,
1✔
818
                        opaque @ CheckResult::Opaque { .. } => {
×
819
                            self.path.pop();
×
820
                            return opaque;
×
821
                        }
822
                    }
823
                }
×
824

825
                self.path.pop();
18✔
826
            }
827

828
            // Elements only in dyn array
829
            for i in min_len..dyn_len {
7✔
830
                self.path.push(PathSegment::Index(i));
1✔
831
                if let Some(dyn_elem) = dyn_val.array_get(i) {
1✔
832
                    self.record_only_left(dyn_elem);
1✔
833
                    any_different = true;
1✔
834
                }
1✔
835
                self.path.pop();
1✔
836
            }
837

838
            // Elements only in other list
839
            for i in min_len..other_len {
7✔
840
                self.path.push(PathSegment::Index(i));
×
841
                if let Some(other_elem) = other_list.get(i) {
×
842
                    self.record_only_right(other_elem);
×
843
                    any_different = true;
×
844
                }
×
845
                self.path.pop();
×
846
            }
847

848
            if any_different {
7✔
849
                CheckResult::Different
2✔
850
            } else {
851
                CheckResult::Same
5✔
852
            }
853
        } else {
854
            // Other is not array-like, they're different
855
            self.record_changed(dyn_peek, other);
×
856
            CheckResult::Different
×
857
        }
858
    }
9✔
859

860
    fn check_two_dyn_arrays(
2✔
861
        &mut self,
2✔
862
        _left_peek: Peek<'_, '_>,
2✔
863
        left_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
864
        left_len: usize,
2✔
865
        _right_peek: Peek<'_, '_>,
2✔
866
        right_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
867
        right_len: usize,
2✔
868
    ) -> CheckResult {
2✔
869
        let mut any_different = false;
2✔
870
        let min_len = left_len.min(right_len);
2✔
871

872
        // Compare common elements
873
        for i in 0..min_len {
6✔
874
            self.path.push(PathSegment::Index(i));
6✔
875

876
            if let (Some(left_elem), Some(right_elem)) =
6✔
877
                (left_dyn.array_get(i), right_dyn.array_get(i))
6✔
878
            {
879
                match self.check(left_elem, right_elem) {
6✔
880
                    CheckResult::Same => {}
5✔
881
                    CheckResult::Different => any_different = true,
1✔
882
                    opaque @ CheckResult::Opaque { .. } => {
×
883
                        self.path.pop();
×
884
                        return opaque;
×
885
                    }
886
                }
887
            }
×
888

889
            self.path.pop();
6✔
890
        }
891

892
        // Elements only in left
893
        for i in min_len..left_len {
2✔
894
            self.path.push(PathSegment::Index(i));
×
895
            if let Some(left_elem) = left_dyn.array_get(i) {
×
896
                self.record_only_left(left_elem);
×
897
                any_different = true;
×
898
            }
×
899
            self.path.pop();
×
900
        }
901

902
        // Elements only in right
903
        for i in min_len..right_len {
2✔
904
            self.path.push(PathSegment::Index(i));
×
905
            if let Some(right_elem) = right_dyn.array_get(i) {
×
906
                self.record_only_right(right_elem);
×
907
                any_different = true;
×
908
            }
×
909
            self.path.pop();
×
910
        }
911

912
        if any_different {
2✔
913
            CheckResult::Different
1✔
914
        } else {
915
            CheckResult::Same
1✔
916
        }
917
    }
2✔
918

919
    fn check_dyn_object_against_other(
2✔
920
        &mut self,
2✔
921
        dyn_peek: Peek<'_, '_>,
2✔
922
        dyn_val: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
923
        other: Peek<'_, '_>,
2✔
924
    ) -> CheckResult {
2✔
925
        let dyn_len = dyn_val.object_len().unwrap_or(0);
2✔
926

927
        // Check if other is also a DynamicValue object
928
        if let Ok(other_dyn) = other.into_dynamic_value() {
2✔
929
            if other_dyn.kind() == DynValueKind::Object {
2✔
930
                let other_len = other_dyn.object_len().unwrap_or(0);
2✔
931
                return self.check_two_dyn_objects(
2✔
932
                    dyn_peek, dyn_val, dyn_len, other, other_dyn, other_len,
2✔
933
                );
934
            } else {
935
                self.record_changed(dyn_peek, other);
×
936
                return CheckResult::Different;
×
937
            }
938
        }
×
939

940
        // Check if other is a map
941
        if let Ok(other_map) = other.into_map() {
×
942
            let mut any_different = false;
×
943
            let mut seen_keys = std::collections::HashSet::new();
×
944

945
            // Check all entries in dyn object
946
            for i in 0..dyn_len {
×
947
                if let Some((key, dyn_value)) = dyn_val.object_get_entry(i) {
×
948
                    seen_keys.insert(key.to_owned());
×
949
                    self.path.push(PathSegment::Key(key.to_owned()));
×
950

951
                    // Try to find key in map - need to compare by formatted key
952
                    let mut found = false;
×
953
                    for (map_key, map_value) in other_map.iter() {
×
954
                        if Self::format_value(map_key) == format!("{key:?}") {
×
955
                            found = true;
×
956
                            match self.check(dyn_value, map_value) {
×
957
                                CheckResult::Same => {}
×
958
                                CheckResult::Different => any_different = true,
×
959
                                opaque @ CheckResult::Opaque { .. } => {
×
960
                                    self.path.pop();
×
961
                                    return opaque;
×
962
                                }
963
                            }
964
                            break;
×
965
                        }
×
966
                    }
967

968
                    if !found {
×
969
                        self.record_only_left(dyn_value);
×
970
                        any_different = true;
×
971
                    }
×
972

973
                    self.path.pop();
×
974
                }
×
975
            }
976

977
            // Check keys only in map
978
            for (map_key, map_value) in other_map.iter() {
×
979
                let key_str = Self::format_value(map_key);
×
980
                // Remove quotes for comparison
981
                let key_unquoted = key_str.trim_matches('"');
×
982
                if !seen_keys.contains(key_unquoted) {
×
983
                    self.path.push(PathSegment::Key(key_unquoted.to_owned()));
×
984
                    self.record_only_right(map_value);
×
985
                    any_different = true;
×
986
                    self.path.pop();
×
987
                }
×
988
            }
989

990
            if any_different {
×
991
                CheckResult::Different
×
992
            } else {
993
                CheckResult::Same
×
994
            }
995
        } else if let Ok(other_struct) = other.into_struct() {
×
996
            // Compare DynamicValue object against struct fields
997
            let mut any_different = false;
×
998
            let mut seen_fields = std::collections::HashSet::new();
×
999

1000
            // Check all entries in dyn object against struct fields
1001
            for i in 0..dyn_len {
×
1002
                if let Some((key, dyn_value)) = dyn_val.object_get_entry(i) {
×
1003
                    seen_fields.insert(key.to_owned());
×
1004
                    self.path.push(PathSegment::Key(key.to_owned()));
×
1005

1006
                    if let Ok(struct_value) = other_struct.field_by_name(key) {
×
1007
                        match self.check(dyn_value, struct_value) {
×
1008
                            CheckResult::Same => {}
×
1009
                            CheckResult::Different => any_different = true,
×
1010
                            opaque @ CheckResult::Opaque { .. } => {
×
1011
                                self.path.pop();
×
1012
                                return opaque;
×
1013
                            }
1014
                        }
1015
                    } else {
×
1016
                        self.record_only_left(dyn_value);
×
1017
                        any_different = true;
×
1018
                    }
×
1019

1020
                    self.path.pop();
×
1021
                }
×
1022
            }
1023

1024
            // Check struct fields not in dyn object
1025
            for (field, struct_value) in other_struct.fields() {
×
1026
                if !seen_fields.contains(field.name) {
×
1027
                    self.path.push(PathSegment::Field(field.name));
×
1028
                    self.record_only_right(struct_value);
×
1029
                    any_different = true;
×
1030
                    self.path.pop();
×
1031
                }
×
1032
            }
1033

1034
            if any_different {
×
1035
                CheckResult::Different
×
1036
            } else {
1037
                CheckResult::Same
×
1038
            }
1039
        } else {
1040
            // Other is not object-like, they're different
1041
            self.record_changed(dyn_peek, other);
×
1042
            CheckResult::Different
×
1043
        }
1044
    }
2✔
1045

1046
    fn check_two_dyn_objects(
2✔
1047
        &mut self,
2✔
1048
        _left_peek: Peek<'_, '_>,
2✔
1049
        left_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
1050
        left_len: usize,
2✔
1051
        _right_peek: Peek<'_, '_>,
2✔
1052
        right_dyn: facet_reflect::PeekDynamicValue<'_, '_>,
2✔
1053
        right_len: usize,
2✔
1054
    ) -> CheckResult {
2✔
1055
        let mut any_different = false;
2✔
1056
        let mut seen_keys = std::collections::HashSet::new();
2✔
1057

1058
        // Check all entries in left
1059
        for i in 0..left_len {
4✔
1060
            if let Some((key, left_value)) = left_dyn.object_get_entry(i) {
4✔
1061
                seen_keys.insert(key.to_owned());
4✔
1062
                self.path.push(PathSegment::Key(key.to_owned()));
4✔
1063

1064
                if let Some(right_value) = right_dyn.object_get(key) {
4✔
1065
                    match self.check(left_value, right_value) {
4✔
1066
                        CheckResult::Same => {}
3✔
1067
                        CheckResult::Different => any_different = true,
1✔
1068
                        opaque @ CheckResult::Opaque { .. } => {
×
1069
                            self.path.pop();
×
1070
                            return opaque;
×
1071
                        }
1072
                    }
1073
                } else {
×
1074
                    self.record_only_left(left_value);
×
1075
                    any_different = true;
×
1076
                }
×
1077

1078
                self.path.pop();
4✔
1079
            }
×
1080
        }
1081

1082
        // Check entries only in right
1083
        for i in 0..right_len {
4✔
1084
            if let Some((key, right_value)) = right_dyn.object_get_entry(i)
4✔
1085
                && !seen_keys.contains(key)
4✔
1086
            {
×
1087
                self.path.push(PathSegment::Key(key.to_owned()));
×
1088
                self.record_only_right(right_value);
×
1089
                any_different = true;
×
1090
                self.path.pop();
×
1091
            }
4✔
1092
        }
1093

1094
        if any_different {
2✔
1095
            CheckResult::Different
1✔
1096
        } else {
1097
            CheckResult::Same
1✔
1098
        }
1099
    }
2✔
1100
}
1101

1102
/// Format a character for display with its Unicode codepoint and visual representation.
1103
fn format_char_with_codepoint(c: char) -> String {
×
1104
    // For printable ASCII characters (except space), show the character directly
1105
    if c.is_ascii_graphic() {
×
1106
        format!("'{}' (U+{:04X})", c, c as u32)
×
1107
    } else {
1108
        // For everything else, show escaped form with codepoint
1109
        format!("'\\u{{{:04X}}}' (U+{:04X})", c as u32, c as u32)
×
1110
    }
1111
}
×
1112

1113
/// Explain the confusable differences between two strings that look identical.
1114
/// Uses the `confusables` crate for detection, then shows character-level diff.
1115
fn explain_confusable_differences(left: &str, right: &str) -> Option<String> {
×
1116
    // Strings must be different but normalize to the same skeleton
1117
    if left == right {
×
1118
        return None;
×
1119
    }
×
1120

1121
    // Find character-level differences
1122
    let left_chars: Vec<char> = left.chars().collect();
×
1123
    let right_chars: Vec<char> = right.chars().collect();
×
1124

1125
    use std::fmt::Write;
1126
    let mut out = String::new();
×
1127

1128
    // Find all positions where characters differ
1129
    let mut diffs: Vec<(usize, char, char)> = Vec::new();
×
1130

1131
    let max_len = left_chars.len().max(right_chars.len());
×
1132
    for i in 0..max_len {
×
1133
        let lc = left_chars.get(i);
×
1134
        let rc = right_chars.get(i);
×
1135

1136
        match (lc, rc) {
×
1137
            (Some(&l), Some(&r)) if l != r => {
×
1138
                diffs.push((i, l, r));
×
1139
            }
×
1140
            (Some(&l), None) => {
×
1141
                // Character only in left (will show as deletion)
×
1142
                diffs.push((i, l, '\0'));
×
1143
            }
×
1144
            (None, Some(&r)) => {
×
1145
                // Character only in right (will show as insertion)
×
1146
                diffs.push((i, '\0', r));
×
1147
            }
×
1148
            _ => {}
×
1149
        }
1150
    }
1151

1152
    if diffs.is_empty() {
×
1153
        return None;
×
1154
    }
×
1155

1156
    writeln!(
×
1157
        out,
×
1158
        "(strings are visually confusable but differ in {} position{}):",
1159
        diffs.len(),
×
1160
        if diffs.len() == 1 { "" } else { "s" }
×
1161
    )
1162
    .ok()?;
×
1163

1164
    for (pos, lc, rc) in &diffs {
×
1165
        if *lc == '\0' {
×
1166
            writeln!(
×
1167
                out,
×
1168
                "  [{}]: (missing) vs {}",
1169
                pos,
1170
                format_char_with_codepoint(*rc)
×
1171
            )
1172
            .ok()?;
×
1173
        } else if *rc == '\0' {
×
1174
            writeln!(
×
1175
                out,
×
1176
                "  [{}]: {} vs (missing)",
1177
                pos,
1178
                format_char_with_codepoint(*lc)
×
1179
            )
1180
            .ok()?;
×
1181
        } else {
1182
            writeln!(
×
1183
                out,
×
1184
                "  [{}]: {} vs {}",
1185
                pos,
1186
                format_char_with_codepoint(*lc),
×
1187
                format_char_with_codepoint(*rc)
×
1188
            )
1189
            .ok()?;
×
1190
        }
1191
    }
1192

1193
    Some(out.trim_end().to_string())
×
1194
}
×
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