From 5d97c6a349f975110594b083f4d81c29bb9f0562 Mon Sep 17 00:00:00 2001 From: Jonas Bushart Date: Sat, 5 Oct 2024 21:32:59 +0200 Subject: [PATCH] Add support for hashbrown v0.15 Hashbrown support for v0.14 is already included and this just extends the code to cover the new crate version. Closes #787 --- .github/workflows/ci.yaml | 2 +- Cargo.lock | 10 + serde_with/Cargo.toml | 12 + serde_with/src/de/duplicates.rs | 2 + serde_with/src/de/impls.rs | 13 + serde_with/src/de/skip_error.rs | 2 + .../duplicate_key_impls/error_on_duplicate.rs | 40 ++ .../duplicate_key_impls/first_value_wins.rs | 28 ++ .../duplicate_key_impls/last_value_wins.rs | 21 + serde_with/src/schemars_0_8.rs | 2 + serde_with/src/ser/duplicates.rs | 2 + serde_with/src/ser/impls.rs | 6 + serde_with/src/ser/skip_error.rs | 2 + serde_with/tests/hashbrown_0_15.rs | 388 ++++++++++++++++++ 14 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 serde_with/tests/hashbrown_0_15.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b2190036..7bb7f67d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,7 +88,7 @@ jobs: - name: "Check no_std+alloc (No Default Features / ${{ matrix.os }} / ${{ matrix.rust }})" run: cargo check --package serde_with --no-default-features --features=alloc --target thumbv7em-none-eabihf - name: "Check no_std+alloc+optional (No Default Features / ${{ matrix.os }} / ${{ matrix.rust }})" - run: cargo check --package serde_with --no-default-features --features=alloc,base64,chrono_0_4,hashbrown_0_14,hex,indexmap_1,indexmap_2,json,time_0_3 --target thumbv7em-none-eabihf + run: cargo check --package serde_with --no-default-features --features=alloc,base64,chrono_0_4,hashbrown_0_14,hashbrown_0_15,hex,indexmap_1,indexmap_2,json,time_0_3 --target thumbv7em-none-eabihf # The tests are split into build and run steps, to see the time impact of each # cargo test --all-targets does NOT run doctests diff --git a/Cargo.lock b/Cargo.lock index 2a430265..e66fe11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "serde", +] + [[package]] name = "hex" version = "0.4.3" @@ -881,6 +890,7 @@ dependencies = [ "fnv", "glob", "hashbrown 0.14.3", + "hashbrown 0.15.0", "hex", "indexmap 1.9.3", "indexmap 2.1.0", diff --git a/serde_with/Cargo.toml b/serde_with/Cargo.toml index 1e0ac822..0006d261 100644 --- a/serde_with/Cargo.toml +++ b/serde_with/Cargo.toml @@ -73,6 +73,12 @@ chrono_0_4 = ["dep:chrono_0_4"] ## It enables the `alloc` feature. ## Some functionality is only available when `std` is enabled too. hashbrown_0_14 = ["dep:hashbrown_0_14", "alloc"] +## The feature enables `hashbrown::{HashMap, HashSet}` as supported containers. +## +## This pulls in `hashbrown` v0.15 as a dependency. +## It enables the `alloc` feature. +## Some functionality is only available when `std` is enabled too. +hashbrown_0_15 = ["dep:hashbrown_0_15", "alloc"] ## The feature enables serializing data in hex format. ## ## This pulls in `hex` as a dependency. @@ -126,6 +132,7 @@ chrono_0_4 = {package = "chrono", version = "0.4.20", optional = true, default-f doc-comment = {version = "0.3.3", optional = true} document-features = {version = "0.2.7", optional = true} hashbrown_0_14 = {package = "hashbrown", version = "0.14.0", optional = true, default-features = false, features = ["serde"]} +hashbrown_0_15 = {package = "hashbrown", version = "0.15.0", optional = true, default-features = false, features = ["serde"]} hex = {version = "0.4.3", optional = true, default-features = false} indexmap_1 = {package = "indexmap", version = "1.8", optional = true, default-features = false, features = ["serde-1"]} indexmap_2 = {package = "indexmap", version = "2.0", optional = true, default-features = false, features = ["serde"]} @@ -175,6 +182,11 @@ name = "hashbrown_0_14" path = "tests/hashbrown_0_14.rs" required-features = ["hashbrown_0_14", "macros"] +[[test]] +name = "hashbrown_0_15" +path = "tests/hashbrown_0_15.rs" +required-features = ["hashbrown_0_15", "macros"] + [[test]] name = "indexmap_1" path = "tests/indexmap_1.rs" diff --git a/serde_with/src/de/duplicates.rs b/serde_with/src/de/duplicates.rs index 3dc7b9d8..5a2dde92 100644 --- a/serde_with/src/de/duplicates.rs +++ b/serde_with/src/de/duplicates.rs @@ -8,6 +8,8 @@ use crate::{ }; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014}; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015}; #[cfg(feature = "indexmap_1")] use indexmap_1::{IndexMap, IndexSet}; #[cfg(feature = "indexmap_2")] diff --git a/serde_with/src/de/impls.rs b/serde_with/src/de/impls.rs index 25474208..93b370d5 100644 --- a/serde_with/src/de/impls.rs +++ b/serde_with/src/de/impls.rs @@ -2,6 +2,8 @@ pub(crate) use self::macros::*; use crate::{formats::*, prelude::*}; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014}; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015}; #[cfg(feature = "indexmap_1")] use indexmap_1::{IndexMap, IndexSet}; #[cfg(feature = "indexmap_2")] @@ -32,6 +34,11 @@ pub(crate) mod macros { HashbrownMap014, (|size| HashbrownMap014::with_capacity_and_hasher(size, Default::default())) ); + #[cfg(feature = "hashbrown_0_15")] + $m!( + HashbrownMap015, + (|size| HashbrownMap015::with_capacity_and_hasher(size, Default::default())) + ); #[cfg(feature = "indexmap_1")] $m!( IndexMap, @@ -61,6 +68,12 @@ pub(crate) mod macros { (|size| HashbrownSet014::with_capacity_and_hasher(size, S::default())), insert ); + #[cfg(feature = "hashbrown_0_15")] + $m!( + HashbrownSet015, + (|size| HashbrownSet015::with_capacity_and_hasher(size, S::default())), + insert + ); #[cfg(feature = "indexmap_1")] $m!( IndexSet, diff --git a/serde_with/src/de/skip_error.rs b/serde_with/src/de/skip_error.rs index dd7fa1cb..20f48cf3 100644 --- a/serde_with/src/de/skip_error.rs +++ b/serde_with/src/de/skip_error.rs @@ -2,6 +2,8 @@ use super::impls::macros::foreach_map; use crate::prelude::*; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::HashMap as HashbrownMap014; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::HashMap as HashbrownMap015; #[cfg(feature = "indexmap_1")] use indexmap_1::IndexMap; #[cfg(feature = "indexmap_2")] diff --git a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs index 715c2153..2b80346a 100644 --- a/serde_with/src/duplicate_key_impls/error_on_duplicate.rs +++ b/serde_with/src/duplicate_key_impls/error_on_duplicate.rs @@ -54,6 +54,26 @@ where } } +#[cfg(feature = "hashbrown_0_15")] +impl PreventDuplicateInsertsSet for hashbrown_0_15::HashSet +where + T: Eq + Hash, + S: BuildHasher + Default, +{ + #[inline] + fn new(size_hint: Option) -> Self { + match size_hint { + Some(size) => Self::with_capacity_and_hasher(size, S::default()), + None => Self::with_hasher(S::default()), + } + } + + #[inline] + fn insert(&mut self, value: T) -> bool { + self.insert(value) + } +} + #[cfg(feature = "indexmap_1")] impl PreventDuplicateInsertsSet for indexmap_1::IndexSet where @@ -149,6 +169,26 @@ where } } +#[cfg(feature = "hashbrown_0_15")] +impl PreventDuplicateInsertsMap for hashbrown_0_15::HashMap +where + K: Eq + Hash, + S: BuildHasher + Default, +{ + #[inline] + fn new(size_hint: Option) -> Self { + match size_hint { + Some(size) => Self::with_capacity_and_hasher(size, S::default()), + None => Self::with_hasher(S::default()), + } + } + + #[inline] + fn insert(&mut self, key: K, value: V) -> bool { + self.insert(key, value).is_none() + } +} + #[cfg(feature = "indexmap_1")] impl PreventDuplicateInsertsMap for indexmap_1::IndexMap where diff --git a/serde_with/src/duplicate_key_impls/first_value_wins.rs b/serde_with/src/duplicate_key_impls/first_value_wins.rs index e2413e6b..38f4bff7 100644 --- a/serde_with/src/duplicate_key_impls/first_value_wins.rs +++ b/serde_with/src/duplicate_key_impls/first_value_wins.rs @@ -63,6 +63,34 @@ where } } +#[cfg(feature = "hashbrown_0_15")] +impl DuplicateInsertsFirstWinsMap for hashbrown_0_15::HashMap +where + K: Eq + Hash, + S: BuildHasher + Default, +{ + #[inline] + fn new(size_hint: Option) -> Self { + match size_hint { + Some(size) => Self::with_capacity_and_hasher(size, S::default()), + None => Self::with_hasher(S::default()), + } + } + + #[inline] + fn insert(&mut self, key: K, value: V) { + use hashbrown_0_15::hash_map::Entry; + + match self.entry(key) { + // we want to keep the first value, so do nothing + Entry::Occupied(_) => {} + Entry::Vacant(vacant) => { + vacant.insert(value); + } + } + } +} + #[cfg(feature = "indexmap_1")] impl DuplicateInsertsFirstWinsMap for indexmap_1::IndexMap where diff --git a/serde_with/src/duplicate_key_impls/last_value_wins.rs b/serde_with/src/duplicate_key_impls/last_value_wins.rs index 970197aa..af75f490 100644 --- a/serde_with/src/duplicate_key_impls/last_value_wins.rs +++ b/serde_with/src/duplicate_key_impls/last_value_wins.rs @@ -49,6 +49,27 @@ where } } +#[cfg(feature = "hashbrown_0_15")] +impl DuplicateInsertsLastWinsSet for hashbrown_0_15::HashSet +where + T: Eq + Hash, + S: BuildHasher + Default, +{ + #[inline] + fn new(size_hint: Option) -> Self { + match size_hint { + Some(size) => Self::with_capacity_and_hasher(size, S::default()), + None => Self::with_hasher(S::default()), + } + } + + #[inline] + fn replace(&mut self, value: T) { + // Hashset already fulfils the contract + self.replace(value); + } +} + #[cfg(feature = "indexmap_1")] impl DuplicateInsertsLastWinsSet for indexmap_1::IndexSet where diff --git a/serde_with/src/schemars_0_8.rs b/serde_with/src/schemars_0_8.rs index 603bffb4..0864c4eb 100644 --- a/serde_with/src/schemars_0_8.rs +++ b/serde_with/src/schemars_0_8.rs @@ -787,6 +787,8 @@ map_first_last_wins_schema!(BTreeMap); map_first_last_wins_schema!(=> S HashMap); #[cfg(feature = "hashbrown_0_14")] map_first_last_wins_schema!(=> S hashbrown_0_14::HashMap); +#[cfg(feature = "hashbrown_0_15")] +map_first_last_wins_schema!(=> S hashbrown_0_15::HashMap); #[cfg(feature = "indexmap_1")] map_first_last_wins_schema!(=> S indexmap_1::IndexMap); #[cfg(feature = "indexmap_2")] diff --git a/serde_with/src/ser/duplicates.rs b/serde_with/src/ser/duplicates.rs index 8c837835..dc89b492 100644 --- a/serde_with/src/ser/duplicates.rs +++ b/serde_with/src/ser/duplicates.rs @@ -2,6 +2,8 @@ use super::impls::macros::{foreach_map, foreach_set}; use crate::prelude::*; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014}; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015}; #[cfg(feature = "indexmap_1")] use indexmap_1::{IndexMap, IndexSet}; #[cfg(feature = "indexmap_2")] diff --git a/serde_with/src/ser/impls.rs b/serde_with/src/ser/impls.rs index 03ae0b3d..7fc32900 100644 --- a/serde_with/src/ser/impls.rs +++ b/serde_with/src/ser/impls.rs @@ -2,6 +2,8 @@ pub(crate) use self::macros::*; use crate::{formats::Strictness, prelude::*}; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::{HashMap as HashbrownMap014, HashSet as HashbrownSet014}; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::{HashMap as HashbrownMap015, HashSet as HashbrownSet015}; #[cfg(feature = "indexmap_1")] use indexmap_1::{IndexMap, IndexSet}; #[cfg(feature = "indexmap_2")] @@ -29,6 +31,8 @@ pub(crate) mod macros { $m!(HashMap); #[cfg(feature = "hashbrown_0_14")] $m!(HashbrownMap014); + #[cfg(feature = "hashbrown_0_15")] + $m!(HashbrownMap015); #[cfg(feature = "indexmap_1")] $m!(IndexMap); #[cfg(feature = "indexmap_2")] @@ -44,6 +48,8 @@ pub(crate) mod macros { $m!(HashSet<$T, H: Sized>); #[cfg(feature = "hashbrown_0_14")] $m!(HashbrownSet014<$T, H: Sized>); + #[cfg(feature = "hashbrown_0_15")] + $m!(HashbrownSet015<$T, H: Sized>); #[cfg(feature = "indexmap_1")] $m!(IndexSet<$T, H: Sized>); #[cfg(feature = "indexmap_2")] diff --git a/serde_with/src/ser/skip_error.rs b/serde_with/src/ser/skip_error.rs index 07c8c99f..0eea6e09 100644 --- a/serde_with/src/ser/skip_error.rs +++ b/serde_with/src/ser/skip_error.rs @@ -2,6 +2,8 @@ use super::impls::macros::foreach_map; use crate::prelude::*; #[cfg(feature = "hashbrown_0_14")] use hashbrown_0_14::HashMap as HashbrownMap014; +#[cfg(feature = "hashbrown_0_15")] +use hashbrown_0_15::HashMap as HashbrownMap015; #[cfg(feature = "indexmap_1")] use indexmap_1::IndexMap; #[cfg(feature = "indexmap_2")] diff --git a/serde_with/tests/hashbrown_0_15.rs b/serde_with/tests/hashbrown_0_15.rs new file mode 100644 index 00000000..4352d58b --- /dev/null +++ b/serde_with/tests/hashbrown_0_15.rs @@ -0,0 +1,388 @@ +//! Test Cases + +mod utils; + +use crate::utils::{check_deserialization, check_error_deserialization, is_equal}; +use expect_test::expect; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr, Same}; +use std::net::IpAddr; + +type HashMap = hashbrown_0_15::HashMap; +type HashSet = hashbrown_0_15::HashSet; + +#[test] +fn test_hashmap() { + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S(#[serde_as(as = "HashMap")] HashMap); + + // Normal + is_equal( + S([(1, 1), (3, 3), (111, 111)].iter().copied().collect()), + expect![[r#" + { + "1": "1", + "3": "3", + "111": "111" + }"#]], + ); + is_equal(S(HashMap::default()), expect![[r#"{}"#]]); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SStd( + #[serde_as( + as = "HashMap>" + )] + HashMap>, + ); + + // Normal + is_equal( + SStd([(1, 1)].iter().copied().collect()), + expect![[r#" + { + "1": "1" + }"#]], + ); + is_equal(SStd(HashMap::default()), expect![[r#"{}"#]]); +} + +#[test] +fn test_hashset() { + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct S(#[serde_as(as = "HashSet")] HashSet); + + // Normal + is_equal( + S([1, 2, 3, 4, 5].iter().copied().collect()), + expect![[r#" + [ + "5", + "4", + "1", + "3", + "2" + ]"#]], + ); + is_equal(S(HashSet::default()), expect![[r#"[]"#]]); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SStd( + #[serde_as( + as = "HashSet>" + )] + HashSet>, + ); + + // Normal + is_equal( + SStd([1].iter().copied().collect()), + expect![[r#" + [ + "1" + ]"#]], + ); + is_equal(SStd(HashSet::default()), expect![[r#"[]"#]]); +} + +#[test] +fn test_map_as_tuple_list() { + let ip = "1.2.3.4".parse().unwrap(); + let ip2 = "255.255.255.255".parse().unwrap(); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SI(#[serde_as(as = "Vec<(DisplayFromStr, DisplayFromStr)>")] HashMap); + + let map: HashMap<_, _> = vec![(1, ip), (10, ip), (200, ip2)].into_iter().collect(); + is_equal( + SI(map.clone()), + expect![[r#" + [ + [ + "1", + "1.2.3.4" + ], + [ + "200", + "255.255.255.255" + ], + [ + "10", + "1.2.3.4" + ] + ]"#]], + ); + + #[serde_as] + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct SI2(#[serde_as(as = "Vec<(Same, DisplayFromStr)>")] HashMap); + + is_equal( + SI2(map), + expect![[r#" + [ + [ + 1, + "1.2.3.4" + ], + [ + 200, + "255.255.255.255" + ], + [ + 10, + "1.2.3.4" + ] + ]"#]], + ); +} + +#[test] +fn duplicate_key_first_wins_hashmap() { + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S(#[serde(with = "::serde_with::rust::maps_first_key_wins")] HashMap); + + // Different value and key always works + is_equal( + S(HashMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])), + expect![[r#" + { + "1": 1, + "3": 3, + "2": 2 + }"#]], + ); + + // Same value for different keys is ok + is_equal( + S(HashMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])), + expect![[r#" + { + "1": 1, + "3": 1, + "2": 1 + }"#]], + ); + + // Duplicate keys, the first one is used + check_deserialization( + S(HashMap::from_iter(vec![(1, 1), (2, 2)])), + r#"{"1": 1, "2": 2, "1": 3}"#, + ); +} + +#[test] +fn prohibit_duplicate_key_hashmap() { + #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] + struct S( + #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] HashMap, + ); + + // Different value and key always works + is_equal( + S(HashMap::from_iter(vec![(1, 1), (2, 2), (3, 3)])), + expect![[r#" + { + "1": 1, + "3": 3, + "2": 2 + }"#]], + ); + + // Same value for different keys is ok + is_equal( + S(HashMap::from_iter(vec![(1, 1), (2, 1), (3, 1)])), + expect![[r#" + { + "1": 1, + "3": 1, + "2": 1 + }"#]], + ); + + // Duplicate keys are an error + check_error_deserialization::( + r#"{"1": 1, "2": 2, "1": 3}"#, + expect![[r#"invalid entry: found duplicate key at line 1 column 24"#]], + ); +} + +#[test] +fn duplicate_value_last_wins_hashset() { + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S(#[serde(with = "::serde_with::rust::sets_last_value_wins")] HashSet); + + #[derive(Debug, Eq, Deserialize, Serialize)] + struct W(i32, bool); + impl PartialEq for W { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + impl std::hash::Hash for W { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + self.0.hash(state); + } + } + + // Different values always work + is_equal( + S(HashSet::from_iter(vec![ + W(1, true), + W(2, false), + W(3, true), + ])), + expect![[r#" + [ + [ + 1, + true + ], + [ + 3, + true + ], + [ + 2, + false + ] + ]"#]], + ); + + let value: S = serde_json::from_str( + r#"[ + [1, false], + [1, true], + [2, true], + [2, false] + ]"#, + ) + .unwrap(); + let entries: Vec<_> = value.0.into_iter().collect(); + assert_eq!(1, entries[0].0); + assert!(entries[0].1); + assert_eq!(2, entries[1].0); + assert!(!entries[1].1); +} + +#[test] +fn prohibit_duplicate_value_hashset() { + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S(#[serde(with = "::serde_with::rust::sets_duplicate_value_is_error")] HashSet); + + is_equal( + S(HashSet::from_iter(vec![1, 2, 3, 4])), + expect![[r#" + [ + 4, + 1, + 3, + 2 + ]"#]], + ); + check_error_deserialization::( + r#"[1, 2, 3, 4, 1]"#, + expect![[r#"invalid entry: found duplicate value at line 1 column 15"#]], + ); +} + +#[test] +fn test_map_skip_error_hashmap() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "values": { + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + } + }"#, + ); + check_error_deserialization::( + r#"{"tag":"type", "values":{"0": 1,}}"#, + expect!["trailing comma at line 1 column 33"], + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "values": { + "255": 255, + "0": 0 + } + }"#]], + ); +} + +#[test] +fn test_map_skip_error_hashmap_flatten() { + use serde_with::MapSkipError; + + #[serde_as] + #[derive(Debug, PartialEq, Deserialize, Serialize)] + struct S { + tag: String, + #[serde_as(as = "MapSkipError")] + #[serde(flatten)] + values: HashMap, + } + + check_deserialization( + S { + tag: "type".into(), + values: [(0, 1), (10, 20)].into_iter().collect(), + }, + r#" + { + "tag":"type", + "0": 1, + "str": 2, + "3": "str", + "4": [10, 11], + "5": {}, + "10": 20 + }"#, + ); + is_equal( + S { + tag: "round-trip".into(), + values: [(0, 0), (255, 255)].into_iter().collect(), + }, + expect![[r#" + { + "tag": "round-trip", + "255": 255, + "0": 0 + }"#]], + ); +}