Skip to main content

foundry_cheatcodes/
version.rs

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