From 921f1ab203cd3014c428154f653f3a14539199ab Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Wed, 17 Jan 2024 03:19:21 +0400 Subject: [PATCH] assertApproxEqRel cheatcodes --- crates/cheatcodes/assets/cheatcodes.json | 160 +++++++++++++++++++ crates/cheatcodes/spec/src/vm.rs | 68 ++++++++ crates/cheatcodes/src/test/assert.rs | 194 +++++++++++++++++++++-- testdata/cheats/Vm.sol | 8 + 4 files changed, 418 insertions(+), 12 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 5e2aedc741397..65ede85046fe9 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -653,6 +653,166 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "assertApproxEqRelDecimal_0", + "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message.", + "declaration": "function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRelDecimal(uint256,uint256,uint256,uint256)", + "selector": "0x21ed2977", + "selectorBytes": [ + 33, + 237, + 41, + 119 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRelDecimal_1", + "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message. Includes error message into revert string on failure.", + "declaration": "function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRelDecimal(uint256,uint256,uint256,uint256,string)", + "selector": "0x82d6c8fd", + "selectorBytes": [ + 130, + 214, + 200, + 253 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRelDecimal_2", + "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message.", + "declaration": "function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRelDecimal(int256,int256,uint256,uint256)", + "selector": "0xabbf21cc", + "selectorBytes": [ + 171, + 191, + 33, + 204 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRelDecimal_3", + "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nFormats values with decimals in failure message. Includes error message into revert string on failure.", + "declaration": "function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRelDecimal(int256,int256,uint256,uint256,string)", + "selector": "0xfccc11c4", + "selectorBytes": [ + 252, + 204, + 17, + 196 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRel_0", + "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%", + "declaration": "function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRel(uint256,uint256,uint256)", + "selector": "0x8cf25ef4", + "selectorBytes": [ + 140, + 242, + 94, + 244 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRel_1", + "description": "Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nIncludes error message into revert string on failure.", + "declaration": "function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRel(uint256,uint256,uint256,string)", + "selector": "0x1ecb7d33", + "selectorBytes": [ + 30, + 203, + 125, + 51 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRel_2", + "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%", + "declaration": "function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRel(int256,int256,uint256)", + "selector": "0xfea2d14f", + "selectorBytes": [ + 254, + 162, + 209, + 79 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "assertApproxEqRel_3", + "description": "Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`.\n`maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100%\nIncludes error message into revert string on failure.", + "declaration": "function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure;", + "visibility": "external", + "mutability": "pure", + "signature": "assertApproxEqRel(int256,int256,uint256,string)", + "selector": "0xef277d72", + "selectorBytes": [ + 239, + 39, + 125, + 114 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "assertEqDecimal_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 990c83cdf2009..ceeca01952916 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1169,6 +1169,74 @@ interface Vm { string calldata error ) external pure; + /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta) external pure; + + /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Includes error message into revert string on failure. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure; + + /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta) external pure; + + /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Includes error message into revert string on failure. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; + + /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Formats values with decimals in failure message. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRelDecimal( + uint256 left, + uint256 right, + uint256 maxPercentDelta, + uint256 decimals + ) external pure; + + /// Compares two `uint256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Formats values with decimals in failure message. Includes error message into revert string on failure. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRelDecimal( + uint256 left, + uint256 right, + uint256 maxPercentDelta, + uint256 decimals, + string calldata error + ) external pure; + + /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Formats values with decimals in failure message. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRelDecimal( + int256 left, + int256 right, + uint256 maxPercentDelta, + uint256 decimals + ) external pure; + + /// Compares two `int256` values. Expects relative difference in percents to be less than or equal to `maxPercentDelta`. + /// `maxPercentDelta` is an 18 decimal fixed point number, where 1e18 == 100% + /// Formats values with decimals in failure message. Includes error message into revert string on failure. + #[cheatcode(group = Testing, safety = Safe)] + function assertApproxEqRelDecimal( + int256 left, + int256 right, + uint256 maxPercentDelta, + uint256 decimals, + string calldata error + ) external pure; + // ======== OS and Filesystem ======== // -------- Metadata -------- diff --git a/crates/cheatcodes/src/test/assert.rs b/crates/cheatcodes/src/test/assert.rs index 06ba1902abda7..333d864309d66 100644 --- a/crates/cheatcodes/src/test/assert.rs +++ b/crates/cheatcodes/src/test/assert.rs @@ -6,6 +6,8 @@ use itertools::Itertools; use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; +const EQ_REL_DELTA_RESOLUTION: U256 = U256::from_limbs([18, 0, 0, 0]); + #[derive(Debug, thiserror::Error)] #[error("Assertion failed")] struct SimpleAssertionError; @@ -93,6 +95,61 @@ impl EqAbsAssertionError { } } +fn format_delta_percent(delta: &U256) -> String { + format!("{}%", format_units_uint(delta, &(EQ_REL_DELTA_RESOLUTION - U256::from(2)))) +} + +#[derive(thiserror::Error, Debug)] +#[error( + "{left} !~= {right} (max delta: {}, real delta: {})", + format_delta_percent(max_delta), + format_delta_percent(real_delta) +)] +struct EqRelAssertionFailure { + left: T, + right: T, + max_delta: U256, + real_delta: U256, +} + +#[derive(thiserror::Error, Debug)] +enum EqRelAssertionError { + #[error(transparent)] + Failure(Box>), + #[error("Overflow in delta calculation")] + Overflow, +} + +impl EqRelAssertionError { + fn format_with_decimals(&self, decimals: &U256) -> String { + match self { + Self::Failure(f) => format!( + "{} !~= {} (max delta: {}, real delta: {})", + format_units_uint(&f.left, decimals), + format_units_uint(&f.right, decimals), + format_delta_percent(&f.max_delta), + format_delta_percent(&f.real_delta), + ), + Self::Overflow => self.to_string(), + } + } +} + +impl EqRelAssertionError { + fn format_with_decimals(&self, decimals: &U256) -> String { + match self { + Self::Failure(f) => format!( + "{} !~= {} (max delta: {}, real delta: {})", + format_units_int(&f.left, decimals), + format_units_int(&f.right, decimals), + format_delta_percent(&f.max_delta), + format_delta_percent(&f.real_delta), + ), + Self::Overflow => self.to_string(), + } + } +} + type ComparisonResult<'a, T> = Result, ComparisonAssertionError<'a, T>>; impl Cheatcode for assertTrue_0Call { @@ -907,6 +964,62 @@ impl Cheatcode for assertApproxEqAbsDecimal_3Call { } } +impl Cheatcode for assertApproxEqRel_0Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(uint_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("Assertion failed: {}", e))?) + } +} + +impl Cheatcode for assertApproxEqRel_1Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(uint_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("{}: {}", self.error, e))?) + } +} + +impl Cheatcode for assertApproxEqRel_2Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(int_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("Assertion failed: {}", e))?) + } +} + +impl Cheatcode for assertApproxEqRel_3Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(int_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("{}: {}", self.error, e))?) + } +} + +impl Cheatcode for assertApproxEqRelDecimal_0Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(uint_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("Assertion failed: {}", e.format_with_decimals(&self.decimals)))?) + } +} + +impl Cheatcode for assertApproxEqRelDecimal_1Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(uint_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("{}: {}", self.error, e.format_with_decimals(&self.decimals)))?) + } +} + +impl Cheatcode for assertApproxEqRelDecimal_2Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(int_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("Assertion failed: {}", e.format_with_decimals(&self.decimals)))?) + } +} + +impl Cheatcode for assertApproxEqRelDecimal_3Call { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + Ok(int_assert_approx_eq_rel(self.left, self.right, self.maxPercentDelta) + .map_err(|e| format!("{}: {}", self.error, e.format_with_decimals(&self.decimals)))?) + } +} + fn assert_true(condition: bool) -> Result, SimpleAssertionError> { if condition { Ok(Default::default()) @@ -939,12 +1052,35 @@ fn assert_not_eq<'a, T: PartialEq>(left: &'a T, right: &'a T) -> ComparisonResul } } +fn get_delta_uint(left: U256, right: U256) -> U256 { + if left > right { + left - right + } else { + right - left + } +} + +fn get_delta_int(left: I256, right: I256) -> U256 { + let (left_sign, left_abs) = left.into_sign_and_abs(); + let (right_sign, right_abs) = right.into_sign_and_abs(); + + if left_sign == right_sign { + if left_abs > right_abs { + left_abs - right_abs + } else { + right_abs - left_abs + } + } else { + left_abs + right_abs + } +} + fn uint_assert_approx_eq_abs( left: U256, right: U256, max_delta: U256, ) -> Result, Box>> { - let delta = if left > right { left - right } else { right - left }; + let delta = get_delta_uint(left, right); if delta <= max_delta { Ok(Default::default()) @@ -958,23 +1094,57 @@ fn int_assert_approx_eq_abs( right: I256, max_delta: U256, ) -> Result, Box>> { - let (left_sign, left_abs) = left.into_sign_and_abs(); - let (right_sign, right_abs) = right.into_sign_and_abs(); + let delta = get_delta_int(left, right); - let delta = if left_sign == right_sign { - if left_abs > right_abs { - left_abs - right_abs - } else { - right_abs - left_abs - } + if delta <= max_delta { + Ok(Default::default()) } else { - left_abs + right_abs - }; + Err(Box::new(EqAbsAssertionError { left, right, max_delta, real_delta: delta })) + } +} + +fn uint_assert_approx_eq_rel( + left: U256, + right: U256, + max_delta: U256, +) -> Result, EqRelAssertionError> { + let delta = get_delta_uint(left, right) + .checked_mul(U256::pow(U256::from(10), EQ_REL_DELTA_RESOLUTION)) + .ok_or(EqRelAssertionError::Overflow)? / + right; if delta <= max_delta { Ok(Default::default()) } else { - Err(Box::new(EqAbsAssertionError { left, right, max_delta, real_delta: delta })) + Err(EqRelAssertionError::Failure(Box::new(EqRelAssertionFailure { + left, + right, + max_delta, + real_delta: delta, + }))) + } +} + +fn int_assert_approx_eq_rel( + left: I256, + right: I256, + max_delta: U256, +) -> Result, EqRelAssertionError> { + let (_, abs_right) = right.into_sign_and_abs(); + let delta = get_delta_int(left, right) + .checked_mul(U256::pow(U256::from(10), EQ_REL_DELTA_RESOLUTION)) + .ok_or(EqRelAssertionError::Overflow)? / + abs_right; + + if delta <= max_delta { + Ok(Default::default()) + } else { + Err(EqRelAssertionError::Failure(Box::new(EqRelAssertionFailure { + left, + right, + max_delta, + real_delta: delta, + }))) } } diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index add5db213bdf1..d120fd70270b0 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -30,6 +30,14 @@ interface Vm { function assertApproxEqAbs(uint256 left, uint256 right, uint256 maxDelta, string calldata error) external pure; function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta) external pure; function assertApproxEqAbs(int256 left, int256 right, uint256 maxDelta, string calldata error) external pure; + function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals) external pure; + function assertApproxEqRelDecimal(uint256 left, uint256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals) external pure; + function assertApproxEqRelDecimal(int256 left, int256 right, uint256 maxPercentDelta, uint256 decimals, string calldata error) external pure; + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta) external pure; + function assertApproxEqRel(uint256 left, uint256 right, uint256 maxPercentDelta, string calldata error) external pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta) external pure; + function assertApproxEqRel(int256 left, int256 right, uint256 maxPercentDelta, string calldata error) external pure; function assertEqDecimal(uint256 left, uint256 right, uint256 decimals) external pure; function assertEqDecimal(uint256 left, uint256 right, uint256 decimals, string calldata error) external pure; function assertEqDecimal(int256 left, int256 right, uint256 decimals) external pure;