Skip to main content

chrome_for_testing/api/
version.rs

1use rootcause::{Report, report};
2use serde::de::{self, Visitor};
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::cmp::Ordering;
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7
8/// Error returned when parsing a version string fails.
9#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
10#[error("{message}")]
11pub struct ParseVersionError {
12    message: String,
13}
14
15/// A version identifier.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Version {
18    /// The major version number.
19    pub major: u32,
20
21    /// The minor version number.
22    pub minor: u32,
23
24    /// The patch version number.
25    pub patch: u32,
26
27    /// The build version number.
28    pub build: u32,
29}
30
31impl Display for Version {
32    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33        f.write_fmt(format_args!(
34            "{}.{}.{}.{}",
35            self.major, self.minor, self.patch, self.build
36        ))
37    }
38}
39
40impl Ord for Version {
41    fn cmp(&self, other: &Self) -> Ordering {
42        self.major
43            .cmp(&other.major)
44            .then(self.minor.cmp(&other.minor))
45            .then(self.patch.cmp(&other.patch))
46            .then(self.build.cmp(&other.build))
47    }
48}
49
50impl PartialOrd for Version {
51    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52        Some(self.cmp(other))
53    }
54}
55
56fn parse_version(value: &str) -> Result<Version, String> {
57    fn parse_part<'i>(
58        parts: &mut impl Iterator<Item = &'i str>,
59        named: &'static str,
60    ) -> Result<u32, String> {
61        parts
62            .next()
63            .ok_or_else(|| format!("Did not find part '{named}'."))?
64            .parse::<u32>()
65            .map_err(|err| format!("Failed to parse '{named}' part as an u32: {err}"))
66    }
67
68    let mut parts = value.split('.');
69    let major = parse_part(&mut parts, "major")?;
70    let minor = parse_part(&mut parts, "minor")?;
71    let patch = parse_part(&mut parts, "patch")?;
72    let build = parse_part(&mut parts, "build")?;
73
74    if let Some(next) = parts.next() {
75        return Err(format!(
76            "Invalid version string format. Did not expect any additional parts. Got at least the additional part: {next}"
77        ));
78    }
79
80    Ok(Version {
81        major,
82        minor,
83        patch,
84        build,
85    })
86}
87
88impl FromStr for Version {
89    type Err = Report<ParseVersionError>;
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        parse_version(s).map_err(|message| report!(ParseVersionError { message }))
93    }
94}
95
96impl Serialize for Version {
97    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98    where
99        S: Serializer,
100    {
101        serializer.serialize_str(&self.to_string())
102    }
103}
104
105impl<'de> Deserialize<'de> for Version {
106    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
107    where
108        D: Deserializer<'de>,
109    {
110        struct VersionVisitor;
111
112        impl Visitor<'_> for VersionVisitor {
113            type Value = Version;
114
115            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
116                formatter.write_str("a version string in dot format \"{major}.{minor}.{patch}.{build}\", like `1.0.0.0`, with each part being a `u32`, and all parts being required")
117            }
118
119            fn visit_str<E>(self, value: &str) -> Result<Version, E>
120            where
121                E: de::Error,
122            {
123                parse_version(value).map_err(de::Error::custom)
124            }
125        }
126
127        deserializer.deserialize_str(VersionVisitor)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use assertr::prelude::*;
135    use std::collections::HashSet;
136
137    #[test]
138    fn deserialize_valid_version() {
139        let test_cases = vec![
140            (
141                "0.0.0.0",
142                Version {
143                    major: 0,
144                    minor: 0,
145                    patch: 0,
146                    build: 0,
147                },
148            ),
149            (
150                "1.2.3.4",
151                Version {
152                    major: 1,
153                    minor: 2,
154                    patch: 3,
155                    build: 4,
156                },
157            ),
158            (
159                "115.785.5763.42",
160                Version {
161                    major: 115,
162                    minor: 785,
163                    patch: 5763,
164                    build: 42,
165                },
166            ),
167            (
168                "4294967295.4294967295.4294967295.4294967295",
169                Version {
170                    major: u32::MAX,
171                    minor: u32::MAX,
172                    patch: u32::MAX,
173                    build: u32::MAX,
174                },
175            ),
176        ];
177
178        for (input, expected) in test_cases {
179            let json = format!(
180                r#"
181                "{input}"
182            "#
183            );
184            let result = serde_json::from_str::<Version>(&json);
185            assert_that!(result).is_ok().is_equal_to(expected);
186        }
187    }
188
189    #[test]
190    fn deserialize_invalid_version() {
191        let invalid_cases = vec![
192            ("", "Failed to parse 'major' part as an u32"),
193            ("1", "Did not find part 'minor'"),
194            ("1.2", "Did not find part 'patch'"),
195            ("1.2.3", "Did not find part 'build'"),
196            ("1.2.3.4.5", "Did not expect any additional parts"),
197            ("a.b.c.d", "Failed to parse 'major' part as an u32"),
198            ("1.b.c.d", "Failed to parse 'minor' part as an u32"),
199            ("1.2.c.d", "Failed to parse 'patch' part as an u32"),
200            ("1.2.3.d", "Failed to parse 'build' part as an u32"),
201            ("-1.2.3.4", "Failed to parse 'major' part as an u32"),
202            ("1.-2.3.4", "Failed to parse 'minor' part as an u32"),
203            ("1.2.-3.4", "Failed to parse 'patch' part as an u32"),
204            ("1.2.3.-4", "Failed to parse 'build' part as an u32"),
205            ("4294967296.0.0.0", "Failed to parse 'major' part as an u32"),
206            ("0.4294967296.0.0", "Failed to parse 'minor' part as an u32"),
207            ("0.0.4294967296.0", "Failed to parse 'patch' part as an u32"),
208            ("0.0.0.4294967296", "Failed to parse 'build' part as an u32"),
209            (".2.3.4", "Failed to parse 'major' part as an u32"),
210            ("1..3.4", "Failed to parse 'minor' part as an u32"),
211            ("1.2..4", "Failed to parse 'patch' part as an u32"),
212            ("1.2.3.", "Failed to parse 'build' part as an u32"),
213        ];
214
215        for (input, expected_error_substring) in invalid_cases {
216            let json = format!(
217                r#"
218                "{input}"
219            "#
220            );
221            let result = serde_json::from_str::<Version>(&json);
222            assert_that!(result)
223                .is_err()
224                .derive(std::string::ToString::to_string)
225                .contains(expected_error_substring);
226        }
227    }
228
229    #[test]
230    fn display_value() {
231        let version = Version {
232            major: 115,
233            minor: 785,
234            patch: 5763,
235            build: 42,
236        };
237        assert_that!(version).has_display_value("115.785.5763.42");
238    }
239
240    #[test]
241    fn debug_value() {
242        let version = Version {
243            major: 115,
244            minor: 785,
245            patch: 5763,
246            build: 42,
247        };
248        assert_that!(version)
249            .has_debug_value("Version { major: 115, minor: 785, patch: 5763, build: 42 }");
250    }
251
252    #[test]
253    fn comparisons() {
254        let v1 = Version {
255            major: 1,
256            minor: 0,
257            patch: 0,
258            build: 0,
259        };
260        let v2 = Version {
261            major: 1,
262            minor: 0,
263            patch: 0,
264            build: 1,
265        };
266        let v3 = Version {
267            major: 1,
268            minor: 0,
269            patch: 1,
270            build: 0,
271        };
272        let v4 = Version {
273            major: 1,
274            minor: 1,
275            patch: 0,
276            build: 0,
277        };
278        let v5 = Version {
279            major: 2,
280            minor: 0,
281            patch: 0,
282            build: 0,
283        };
284
285        assert_that!(v1).is_less_than(v2);
286        assert_that!(v1).is_less_than(v3);
287        assert_that!(v1).is_less_than(v4);
288        assert_that!(v1).is_less_than(v5);
289        assert_that!(v2).is_less_than(v3);
290        assert_that!(v2).is_less_than(v4);
291        assert_that!(v2).is_less_than(v5);
292        assert_that!(v3).is_less_than(v4);
293        assert_that!(v3).is_less_than(v5);
294        assert_that!(v4).is_less_than(v5);
295
296        assert_that!(v2).is_greater_than(v1);
297        assert_that!(v3).is_greater_than(v1);
298        assert_that!(v4).is_greater_than(v1);
299        assert_that!(v5).is_greater_than(v1);
300        assert_that!(v3).is_greater_than(v2);
301        assert_that!(v4).is_greater_than(v2);
302        assert_that!(v5).is_greater_than(v2);
303        assert_that!(v4).is_greater_than(v3);
304        assert_that!(v5).is_greater_than(v3);
305        assert_that!(v5).is_greater_than(v4);
306
307        assert_that!(v1).is_equal_to(v1);
308        assert_that!(v1).is_less_or_equal_to(v1);
309        assert_that!(v1).is_greater_or_equal_to(v1);
310
311        assert_that!(v2).is_equal_to(v2);
312        assert_that!(v2).is_less_or_equal_to(v2);
313        assert_that!(v2).is_greater_or_equal_to(v2);
314
315        assert_that!(v3).is_equal_to(v3);
316        assert_that!(v3).is_less_or_equal_to(v3);
317        assert_that!(v3).is_greater_or_equal_to(v3);
318
319        assert_that!(v4).is_equal_to(v4);
320        assert_that!(v4).is_less_or_equal_to(v4);
321        assert_that!(v4).is_greater_or_equal_to(v4);
322
323        assert_that!(v5).is_equal_to(v5);
324        assert_that!(v5).is_less_or_equal_to(v5);
325        assert_that!(v5).is_greater_or_equal_to(v5);
326    }
327
328    #[test]
329    fn parse_valid() {
330        assert_that!("1.2.3.4".parse::<Version>())
331            .is_ok()
332            .is_equal_to(Version {
333                major: 1,
334                minor: 2,
335                patch: 3,
336                build: 4,
337            });
338        assert_that!("0.0.0.0".parse::<Version>())
339            .is_ok()
340            .is_equal_to(Version {
341                major: 0,
342                minor: 0,
343                patch: 0,
344                build: 0,
345            });
346    }
347
348    #[test]
349    fn parse_invalid_fails() {
350        assert_that!("".parse::<Version>()).is_err();
351        assert_that!("1".parse::<Version>()).is_err();
352        assert_that!("1.2".parse::<Version>()).is_err();
353        assert_that!("1.2.3".parse::<Version>()).is_err();
354        assert_that!("1.2.3.4.5".parse::<Version>()).is_err();
355        assert_that!("a.b.c.d".parse::<Version>()).is_err();
356    }
357
358    #[test]
359    fn serialize_round_trip() {
360        let version = Version {
361            major: 132,
362            minor: 0,
363            patch: 6834,
364            build: 83,
365        };
366        let json = serde_json::to_string(&version).unwrap();
367        assert_that!(json.clone()).is_equal_to(String::from("\"132.0.6834.83\""));
368        let deserialized: Version = serde_json::from_str(&json).unwrap();
369        assert_that!(deserialized).is_equal_to(version);
370    }
371
372    #[test]
373    fn ord_sorting() {
374        let v3 = Version {
375            major: 2,
376            minor: 0,
377            patch: 0,
378            build: 0,
379        };
380        let v1 = Version {
381            major: 1,
382            minor: 0,
383            patch: 0,
384            build: 0,
385        };
386        let v2 = Version {
387            major: 1,
388            minor: 1,
389            patch: 0,
390            build: 0,
391        };
392        let mut versions = vec![v3, v1, v2];
393        versions.sort();
394        assert_that!(versions).is_equal_to(vec![v1, v2, v3]);
395    }
396
397    #[test]
398    fn hash_in_set() {
399        let v1 = Version {
400            major: 1,
401            minor: 0,
402            patch: 0,
403            build: 0,
404        };
405        let v2 = Version {
406            major: 1,
407            minor: 0,
408            patch: 0,
409            build: 0,
410        };
411        let mut set = HashSet::new();
412        set.insert(v1);
413        set.insert(v2);
414        assert_that!(set.len()).is_equal_to(1);
415    }
416}