Repository: mitsuhiko/insta (Rust)

Issue:
HashMaps with tuple keys are not sorted, even with setting sort_maps set to true
### What happened?

As the title says. This test should pass when triggered several times:

```rust
#[test]
pub fn insta_sort_order() {
        let mut m = HashMap::new();
        m.insert((1, 3), 4);
        m.insert((2, 3), 4);
        m.insert((1, 4), 4);
        m.insert((3, 3), 4);
        m.insert((9, 3), 4);
        insta::with_settings!({sort_maps =>true}, {
            insta::assert_ron_snapshot!(m);
        });
    }
```

However, this test fails spuriously. Indeed, when reviewing the test result, one can easily see that the map is not sorted. One such result is the following:

```
Expression: m
────────────────────────────────────────
+new results
────────────┬───────────────────────────
          0 │+{
          1 │+  (1, 3): 4,
          2 │+  (1, 4): 4,
          3 │+  (3, 3): 4,
          4 │+  (9, 3): 4,
          5 │+  (2, 3): 4,
          6 │+}
```


### Reproduction steps

_No response_

### Insta Version

1.21

### rustc Version

1.64

### What did you expect?

HashMaps using tuples as key should be sorted before serialization if the appropriate setting is set.


Discussion:
Looking at the code, the issue seems to stem from 

https://github.com/mitsuhiko/insta/blob/89f2c2ff77b3978b1aa6cfe3c6671bb4d11b136a/src/content/serialization.rs#L53


A tuple key has type `Content::Tuple(Vec<Content>)`, and when transformed into the `Key` enum, we get a `Key::Other` variant. Thus, the ordering is basically random.

I wonder if the `Key` enum type is really necessary. `Content` already implements `PartialOrd` and `PartialEq`. I replaced the aforementioned line with 
```rust
                items.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Less));
```

and nothing bad has happened yet. My motivating example however now passes, and the serialization produced by `insta` is sorted correctly.

What do you think ?
The motivation is that `1 < 2` when `Content::I32(1)` and `Content::U64(2)` which otherwise would not be the case.
Thanks for your answer !

Ok, I see. Then the current code could simply be adapted by adding one more `Key` variant as an escape hatch of sorts:

```rust

#[derive(PartialEq, PartialOrd, Debug)]
pub enum Key<'a> {
    U64(u64),
    I64(i64),
    F64(f64),
    U128(u128),
    I128(i128),
    Other(&'a Content),
}

impl<'a> Eq for Key<'a> {}

impl<'a> Ord for Key<'a> {
    fn cmp(&self, other: &Self) -> Ordering {
        self.partial_cmp(other).unwrap_or(Ordering::Less)
    }
}

impl Content {
    pub(crate) fn as_key(&self) -> Key<'_> {
        match self.resolve_inner() {
            Content::Char(val) => Key::U64(*val as u64),
            Content::U16(val) => Key::U64((*val).into()),
            Content::U32(val) => Key::U64((*val).into()),
            Content::U64(val) => Key::U64(*val),
            Content::U128(val) => Key::U128(*val),
            Content::I16(val) => Key::I64((*val).into()),
            Content::I32(val) => Key::I64((*val).into()),
            Content::I64(val) => Key::I64(*val),
            Content::I128(val) => Key::I128(*val),
            Content::F32(val) => Key::F64((*val).into()),
            Content::F64(val) => Key::F64(*val),
            other => Key::Other(other),
        }
    }
}
```
(the `Bool` case now becomes redundant with the Other case)


Thanks for the fix :)



Patch:
diff --git a/src/content/serialization.rs b/src/content/serialization.rs
index e28e138f..cf025b8d 100644
--- a/src/content/serialization.rs
+++ b/src/content/serialization.rs
@@ -50,7 +50,15 @@ impl Content {
     pub(crate) fn sort_maps(&mut self) {
         self.walk(&mut |content| {
             if let Content::Map(ref mut items) = content {
-                items.sort_by(|a, b| a.0.as_key().cmp(&b.0.as_key()));
+                // try to compare by key first, if that fails compare by the
+                // object value.  That way some values normalize, and if we
+                // can't normalize we still have a stable order.
+                items.sort_by(|a, b| match (a.0.as_key(), b.0.as_key()) {
+                    (Key::Other, _) | (_, Key::Other) => {
+                        a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)
+                    }
+                    (ref a, ref b) => a.cmp(b),
+                })
             }
             true
         })


Test patch:
diff --git a/tests/snapshots/test_basic__insta_sort_order.snap b/tests/snapshots/test_basic__insta_sort_order.snap
new file mode 100644
index 00000000..472d1d7d
--- /dev/null
+++ b/tests/snapshots/test_basic__insta_sort_order.snap
@@ -0,0 +1,20 @@
+---
+source: tests/test_basic.rs
+expression: m
+---
+? - 1
+  - 3
+: 4
+? - 1
+  - 4
+: 4
+? - 2
+  - 3
+: 4
+? - 3
+  - 3
+: 4
+? - 9
+  - 3
+: 4
+
diff --git a/tests/test_basic.rs b/tests/test_basic.rs
index 2448cd48..831dc448 100644
--- a/tests/test_basic.rs
+++ b/tests/test_basic.rs
@@ -3,6 +3,7 @@ use insta::assert_json_snapshot;
 #[cfg(feature = "yaml")]
 use insta::assert_yaml_snapshot;
 use insta::{assert_debug_snapshot, assert_display_snapshot};
+use std::collections::HashMap;
 use std::fmt;
 
 #[test]
@@ -89,3 +90,17 @@ fn test_u128_json() {
     let x: u128 = u128::from(u64::MAX) * 2;
     assert_json_snapshot!(&x, @"36893488147419103230");
 }
+
+#[cfg(feature = "yaml")]
+#[test]
+fn insta_sort_order() {
+    let mut m = HashMap::new();
+    m.insert((1, 3), 4);
+    m.insert((2, 3), 4);
+    m.insert((1, 4), 4);
+    m.insert((3, 3), 4);
+    m.insert((9, 3), 4);
+    insta::with_settings!({sort_maps =>true}, {
+        insta::assert_yaml_snapshot!(m);
+    });
+}