Skip to main content

foundry_cheatcodes/
version.rs

1use crate::{Cheatcode, Result, Vm::*};
2use alloy_sol_types::SolValue;
3use foundry_common::version::SEMVER_VERSION;
4use semver::Version;
5use std::cmp::Ordering;
6
7impl Cheatcode for foundryVersionCmpCall {
8    fn apply_context_free(&self) -> Result {
9        let Self { version } = self;
10        foundry_version_cmp(version).map(|cmp| (cmp as i8).abi_encode())
11    }
12}
13
14impl Cheatcode for foundryVersionAtLeastCall {
15    fn apply_context_free(&self) -> Result {
16        let Self { version } = self;
17        foundry_version_cmp(version).map(|cmp| cmp.is_ge().abi_encode())
18    }
19}
20
21fn foundry_version_cmp(version: &str) -> Result<Ordering> {
22    version_cmp(strip_semver_metadata(SEMVER_VERSION), version)
23}
24
25/// Strips pre-release (e.g. `-nightly`, `-dev`) and build metadata
26/// (e.g. `+<sha_short>.<unix_timestamp>.<profile>`) from a version string
27/// so we compare on `MAJOR.MINOR.PATCH` only.
28fn strip_semver_metadata(version: &str) -> &str {
29    version.split(['-', '+']).next().unwrap()
30}
31
32fn version_cmp(version_a: &str, version_b: &str) -> Result<Ordering> {
33    let version_a = parse_version(version_a)?;
34    let version_b = parse_version(version_b)?;
35    Ok(version_a.cmp(&version_b))
36}
37
38fn parse_version(version: &str) -> Result<Version> {
39    let version =
40        Version::parse(version).map_err(|e| fmt_err!("invalid version `{version}`: {e}"))?;
41    if !version.pre.is_empty() {
42        return Err(fmt_err!(
43            "invalid version `{version}`: pre-release versions are not supported"
44        ));
45    }
46    if !version.build.is_empty() {
47        return Err(fmt_err!("invalid version `{version}`: build metadata is not supported"));
48    }
49    Ok(version)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn strips_build_metadata_only() {
58        // Tagged release: `1.7.1+<sha>.<ts>.<profile>`
59        assert_eq!(strip_semver_metadata("1.7.1+abc1234567.1737036656.release"), "1.7.1");
60    }
61
62    #[test]
63    fn strips_pre_release_and_build_metadata() {
64        // Nightly: `1.7.1-nightly+<sha>.<ts>.<profile>`
65        assert_eq!(strip_semver_metadata("1.7.1-nightly+abc1234567.1737036656.release"), "1.7.1");
66        // Dev: `1.7.1-dev+<sha>.<ts>.<profile>`
67        assert_eq!(strip_semver_metadata("1.7.1-dev+abc1234567.1737036656.debug"), "1.7.1");
68    }
69
70    #[test]
71    fn strips_plain_version() {
72        assert_eq!(strip_semver_metadata("1.7.1"), "1.7.1");
73    }
74
75    #[test]
76    fn version_cmp_orders_correctly() {
77        assert_eq!(version_cmp("1.7.1", "1.7.1").unwrap(), Ordering::Equal);
78        assert_eq!(version_cmp("1.7.1", "1.7.0").unwrap(), Ordering::Greater);
79        assert_eq!(version_cmp("1.7.1", "1.7.2").unwrap(), Ordering::Less);
80        assert_eq!(version_cmp("1.7.1", "0.0.1").unwrap(), Ordering::Greater);
81        assert_eq!(version_cmp("1.7.1", "99.0.0").unwrap(), Ordering::Less);
82    }
83
84    #[test]
85    fn parse_version_rejects_pre_release_and_build_metadata() {
86        // User-supplied versions must be plain `MAJOR.MINOR.PATCH`.
87        assert!(parse_version("1.7.1-nightly").is_err());
88        assert!(parse_version("1.7.1+abc").is_err());
89        assert!(parse_version("not-a-version").is_err());
90        assert!(parse_version("1.7.1").is_ok());
91    }
92
93    #[test]
94    fn cmp_works_against_full_semver_version_strings() {
95        // Simulate comparing each shape of `SEMVER_VERSION` against a user-supplied version.
96        for current in [
97            "1.7.1+abc1234567.1737036656.release",
98            "1.7.1-nightly+abc1234567.1737036656.release",
99            "1.7.1-dev+abc1234567.1737036656.debug",
100            "1.7.1",
101        ] {
102            let stripped = strip_semver_metadata(current);
103            assert_eq!(version_cmp(stripped, "1.7.1").unwrap(), Ordering::Equal);
104            assert_eq!(version_cmp(stripped, "1.7.0").unwrap(), Ordering::Greater);
105            assert_eq!(version_cmp(stripped, "1.7.2").unwrap(), Ordering::Less);
106        }
107    }
108}