|
| 1 | +use acceptable::{ |
| 2 | + AllVersions, |
| 3 | + Container, |
| 4 | + CurrentContainer, |
| 5 | + IntoContainer, |
| 6 | + UpgradeError, |
| 7 | +}; |
| 8 | +use serde::Deserialize; |
| 9 | + |
| 10 | +mod v1; |
| 11 | + |
| 12 | +pub use self::v1::RebuildChangedDefinitionsRequestV1; |
| 13 | + |
| 14 | +#[remain::sorted] |
| 15 | +#[derive(AllVersions, CurrentContainer, Clone, Debug, Deserialize, Eq, PartialEq)] |
| 16 | +#[serde(rename_all = "camelCase")] |
| 17 | +pub enum RebuildChangedDefinitionsRequestAllVersions { |
| 18 | + #[acceptable(current)] |
| 19 | + V1(RebuildChangedDefinitionsRequestV1), |
| 20 | +} |
| 21 | + |
| 22 | +impl IntoContainer for RebuildChangedDefinitionsRequestAllVersions { |
| 23 | + type Container = RebuildChangedDefinitionsRequest; |
| 24 | + |
| 25 | + fn into_container(self) -> Result<Self::Container, UpgradeError> { |
| 26 | + match self { |
| 27 | + Self::V1(inner) => Ok(Self::Container::new(inner)), |
| 28 | + } |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +#[cfg(test)] |
| 33 | +mod test { |
| 34 | + use std::{ |
| 35 | + error::Error, |
| 36 | + fmt, |
| 37 | + fs::File, |
| 38 | + io::{ |
| 39 | + self, |
| 40 | + BufRead as _, |
| 41 | + BufReader, |
| 42 | + Read as _, |
| 43 | + }, |
| 44 | + path::Path, |
| 45 | + }; |
| 46 | + |
| 47 | + use serde::{ |
| 48 | + Serialize, |
| 49 | + de::DeserializeOwned, |
| 50 | + }; |
| 51 | + |
| 52 | + /// Tests that a versioned object will always serialize to the same representation, no matter |
| 53 | + /// what future versions or changes to the object. |
| 54 | + /// |
| 55 | + /// NOTE: It is imperative that incremental refactorings do not lead to a version-incompatible |
| 56 | + /// or serialize-incompatible change. If a test fails and it's because there is a diff to the |
| 57 | + /// commiteed `.snap` file, this should be considered a failed refactoring. The remediation is |
| 58 | + /// to *not* update the `.snap` file but rather to fix the code so that the `.snap` format is |
| 59 | + /// 100% preserved. |
| 60 | + pub fn assert_serialize(name: &str, version: u64, serialize: impl Serialize) { |
| 61 | + insta::with_settings!({ |
| 62 | + snapshot_path => format!("rebuild_changed_definitions_request/snapshots-v{version}"), |
| 63 | + prepend_module_to_snapshot => false, |
| 64 | + omit_expression => true, |
| 65 | + description => concat!( |
| 66 | + "\n", |
| 67 | + "\n", |
| 68 | + "!!!\n", |
| 69 | + "!!! System Initiative Developers:\n", |
| 70 | + "!!!\n", |
| 71 | + "!!! IMPORTANT:\n", |
| 72 | + "!!!\n", |
| 73 | + "!!! The contents of this snapshot should *never* be modified as it\n", |
| 74 | + "!!! represents the serialization of a versioned Rust type. If a tests fails\n", |
| 75 | + "!!! with this warning, then something about a Rust type has changed\n", |
| 76 | + "!!! the wire serialization of this type and would represent a potential\n", |
| 77 | + "!!! production outage or data corruption.\n", |
| 78 | + "!!!\n", |
| 79 | + "!!! Consider this an erroneous behavioral change of the Rust code and *not*\n", |
| 80 | + "!!! an out-of-date snapshot or fixture!\n", |
| 81 | + "!!!\n", |
| 82 | + "\n", |
| 83 | + "\n", |
| 84 | + ), |
| 85 | + }, { |
| 86 | + insta::assert_json_snapshot!(name, serialize); |
| 87 | + }); |
| 88 | + } |
| 89 | + |
| 90 | + pub fn assert_deserialize<T>(snapshot_name: &str, version: u64, expected: T) |
| 91 | + where |
| 92 | + T: fmt::Debug + DeserializeOwned + PartialEq, |
| 93 | + { |
| 94 | + let actual: T = read_from_snapshot( |
| 95 | + snapshot_name, |
| 96 | + &format!("rebuild_changed_definitions_request/snapshots-v{version}"), |
| 97 | + ) |
| 98 | + .expect("failed to deserialize from snapshot"); |
| 99 | + |
| 100 | + assert_eq!(actual, expected); |
| 101 | + } |
| 102 | + |
| 103 | + fn read_from_snapshot<T>(name: &str, path: &str) -> Result<T, Box<dyn Error>> |
| 104 | + where |
| 105 | + T: fmt::Debug + DeserializeOwned + PartialEq, |
| 106 | + { |
| 107 | + let glob = format!("{path}/{name}.snap"); |
| 108 | + |
| 109 | + let mut maybe_obj_result = None; |
| 110 | + insta::glob!(&glob, |path| { |
| 111 | + let bytes = read_snapshot_content(path).unwrap(); |
| 112 | + |
| 113 | + maybe_obj_result = Some(serde_json::from_slice(&bytes).map_err(Into::into)); |
| 114 | + }); |
| 115 | + |
| 116 | + match maybe_obj_result { |
| 117 | + Some(object_result) => object_result, |
| 118 | + None => Err(Box::new(io::Error::other(format!( |
| 119 | + "snapshot not found: {glob}" |
| 120 | + )))), |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + // Implementation is adapted from the [`insta::Snapshot::from_file`] function. |
| 125 | + // |
| 126 | + // See: <https://github.com/mitsuhiko/insta/blob/62bb0a3/insta/src/snapshot.rs#L339-L421> |
| 127 | + fn read_snapshot_content(path: &Path) -> Result<Vec<u8>, Box<dyn Error>> { |
| 128 | + let mut f = BufReader::new(File::open(path)?); |
| 129 | + |
| 130 | + // Skip through the snapshot metadata, that being the first YAML document, where YAML |
| 131 | + // documents are delimited with a `"---"` line |
| 132 | + { |
| 133 | + let mut buf = String::new(); |
| 134 | + f.read_line(&mut buf)?; |
| 135 | + if buf.trim_end() == "---" { |
| 136 | + loop { |
| 137 | + let read = f.read_line(&mut buf)?; |
| 138 | + if read == 0 { |
| 139 | + break; |
| 140 | + } |
| 141 | + if buf[buf.len() - read..].trim_end() == "---" { |
| 142 | + break; |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + let mut buf = Vec::new(); |
| 149 | + f.read_to_end(&mut buf)?; |
| 150 | + |
| 151 | + Ok(buf) |
| 152 | + } |
| 153 | +} |
0 commit comments