From f39979d07946113e90758e2d064e8fbf120f8248 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 1 Apr 2026 10:37:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20new=20invariants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fuzz/src/invariant.rs | 219 +++++++++++++++++- .../invariants-assertions/index.md | 13 +- documentation/docs/trident-api/invariants.md | 183 +++++++++++++++ documentation/mkdocs.yml | 2 + 4 files changed, 408 insertions(+), 9 deletions(-) create mode 100644 documentation/docs/trident-api/invariants.md diff --git a/crates/fuzz/src/invariant.rs b/crates/fuzz/src/invariant.rs index 6089668b7..1b108a066 100644 --- a/crates/fuzz/src/invariant.rs +++ b/crates/fuzz/src/invariant.rs @@ -37,25 +37,228 @@ macro_rules! invariant { }; } -/// Checks if two expressions are equal and panics with `InvariantViolation` if not. +/// Checks that two expressions are equal. /// -/// Use this macro to check if two expressions are equal. -/// When the expressions are not equal, it will be counted and collected separately -/// from unexpected panics (bugs in fuzz test code). +/// On failure, displays the actual values of both sides. /// /// # Examples /// /// ```ignore -/// // Simple condition check /// invariant_eq!(balance_after, balance_before - amount); -/// invariant_eq!(account.is_initialized, true, "Account is not initialized"); +/// invariant_eq!(account.owner, program_id, "wrong owner"); /// ``` #[macro_export] macro_rules! invariant_eq { ($a:expr, $b:expr) => { - invariant!($a == $b); + { + let left = &$a; + let right = &$b; + if left != right { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` == `{}`, got: {:?} vs {:?}", + stringify!($a), stringify!($b), left, right + ) + )); + } + } + }; + ($a:expr, $b:expr, $($msg:tt)*) => { + { + let left = &$a; + let right = &$b; + if left != right { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (got: {:?} vs {:?})", format!($($msg)*), left, right) + )); + } + } + }; +} + +/// Checks that two expressions are not equal. +/// +/// On failure, displays the value that both sides unexpectedly share. +/// +/// # Examples +/// +/// ```ignore +/// invariant_ne!(owner, Pubkey::default()); +/// invariant_ne!(balance_after, balance_before, "balance should have changed"); +/// ``` +#[macro_export] +macro_rules! invariant_ne { + ($a:expr, $b:expr) => { + { + let left = &$a; + let right = &$b; + if left == right { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` != `{}`, but both are: {:?}", + stringify!($a), stringify!($b), left + ) + )); + } + } + }; + ($a:expr, $b:expr, $($msg:tt)*) => { + { + let left = &$a; + let right = &$b; + if left == right { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (both are: {:?})", format!($($msg)*), left) + )); + } + } + }; +} + +/// Checks that the first expression is strictly greater than the second. +/// +/// # Examples +/// +/// ```ignore +/// invariant_gt!(balance, 0); +/// invariant_gt!(supply_after, supply_before, "supply should increase after mint"); +/// ``` +#[macro_export] +macro_rules! invariant_gt { + ($a:expr, $b:expr) => { + { + let left = &$a; + let right = &$b; + if !(left > right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` > `{}`, got: {:?} vs {:?}", + stringify!($a), stringify!($b), left, right + ) + )); + } + } }; ($a:expr, $b:expr, $($msg:tt)*) => { - invariant!($a == $b, $($msg)*); + { + let left = &$a; + let right = &$b; + if !(left > right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (got: {:?} vs {:?})", format!($($msg)*), left, right) + )); + } + } + }; +} + +/// Checks that the first expression is greater than or equal to the second. +/// +/// # Examples +/// +/// ```ignore +/// invariant_gte!(balance, minimum_balance); +/// invariant_gte!(lamports, rent_exempt, "account not rent-exempt"); +/// ``` +#[macro_export] +macro_rules! invariant_gte { + ($a:expr, $b:expr) => { + { + let left = &$a; + let right = &$b; + if !(left >= right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` >= `{}`, got: {:?} vs {:?}", + stringify!($a), stringify!($b), left, right + ) + )); + } + } + }; + ($a:expr, $b:expr, $($msg:tt)*) => { + { + let left = &$a; + let right = &$b; + if !(left >= right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (got: {:?} vs {:?})", format!($($msg)*), left, right) + )); + } + } + }; +} + +/// Checks that the first expression is strictly less than the second. +/// +/// # Examples +/// +/// ```ignore +/// invariant_lt!(balance_after, balance_before); +/// invariant_lt!(fee, max_fee, "fee exceeds maximum"); +/// ``` +#[macro_export] +macro_rules! invariant_lt { + ($a:expr, $b:expr) => { + { + let left = &$a; + let right = &$b; + if !(left < right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` < `{}`, got: {:?} vs {:?}", + stringify!($a), stringify!($b), left, right + ) + )); + } + } + }; + ($a:expr, $b:expr, $($msg:tt)*) => { + { + let left = &$a; + let right = &$b; + if !(left < right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (got: {:?} vs {:?})", format!($($msg)*), left, right) + )); + } + } + }; +} + +/// Checks that the first expression is less than or equal to the second. +/// +/// # Examples +/// +/// ```ignore +/// invariant_lte!(withdrawal, balance); +/// invariant_lte!(total_supply, max_supply, "supply overflow"); +/// ``` +#[macro_export] +macro_rules! invariant_lte { + ($a:expr, $b:expr) => { + { + let left = &$a; + let right = &$b; + if !(left <= right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!( + "invariant violation: `{}` <= `{}`, got: {:?} vs {:?}", + stringify!($a), stringify!($b), left, right + ) + )); + } + } + }; + ($a:expr, $b:expr, $($msg:tt)*) => { + { + let left = &$a; + let right = &$b; + if !(left <= right) { + std::panic::panic_any($crate::invariant::InvariantViolation( + format!("{} (got: {:?} vs {:?})", format!($($msg)*), left, right) + )); + } + } }; } diff --git a/documentation/docs/trident-advanced/invariants-assertions/index.md b/documentation/docs/trident-advanced/invariants-assertions/index.md index 5187fbfa5..1b7ec7bb2 100644 --- a/documentation/docs/trident-advanced/invariants-assertions/index.md +++ b/documentation/docs/trident-advanced/invariants-assertions/index.md @@ -16,7 +16,10 @@ invariant!(balance_after == balance_before - amount); // With custom message invariant!(balance > 0, "Balance must be positive"); -invariant!(a == b, "Expected {} but got {}", a, b); + +// Comparison macros (auto-display values on failure) +invariant_eq!(balance_after, balance_before - amount); +invariant_gt!(balance, 0, "Balance must be positive"); ``` !!! note "Invariants vs Regular Panics" @@ -126,4 +129,12 @@ Notes: - Without `--exit-code`, invariant failures and program panics are reported but do not force a non-zero process exit. - Unexpected fuzz-test panics (for example `unwrap()` on `None`) are always treated as runtime errors and fail the run. +## Available Macros + +Beyond the basic `invariant!` macro shown above, Trident provides a full family of comparison macros that automatically display values on failure. + +See the [Invariant Macros API Reference](../../trident-api/invariants.md) for signatures, failure messages, and examples. + +--- + For more complex examples and patterns, see the [Trident Examples](../../trident-examples/trident-examples.md) page. diff --git a/documentation/docs/trident-api/invariants.md b/documentation/docs/trident-api/invariants.md new file mode 100644 index 000000000..2df2b6756 --- /dev/null +++ b/documentation/docs/trident-api/invariants.md @@ -0,0 +1,183 @@ +# Invariant Macros + +Invariant macros are used to define intentional checks in your fuzz tests. When an invariant fails, it is counted and collected separately from unexpected panics, and fuzzing continues to find more issues. + +For conceptual guidance on how and when to use invariants, see [Invariants and Assertions](../trident-advanced/invariants-assertions/index.md). + + +!!! note "Invariants vs Regular Panics" + Only `invariant!` failures are collected and allow fuzzing to continue. Regular panics (like `unwrap()` on `None`) are treated as bugs in your fuzz test and will crash the worker immediately. + + +--- + +## `invariant!` + +Checks a boolean condition. The most general-purpose invariant macro. + +```rust +invariant!(condition); +invariant!(condition, "message with {} formatting", value); +``` + +**Failure message (default):** + +``` +invariant violation: balance > 0 +``` + +**Example:** + +```rust +invariant!(account.is_initialized); +invariant!(balance > 0, "Balance must be positive, got {}", balance); +``` + +--- + +## `invariant_eq!` + +Checks that two expressions are equal. Displays actual values on failure. + +```rust +invariant_eq!(left, right); +invariant_eq!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `balance` == `100`, got: 42 vs 100 +``` + +**Example:** + +```rust +invariant_eq!(balance_after, balance_before - amount); +invariant_eq!(account.owner, program_id, "wrong owner"); +``` + +--- + +## `invariant_ne!` + +Checks that two expressions are not equal. Displays the shared value on failure. + +```rust +invariant_ne!(left, right); +invariant_ne!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `owner` != `Pubkey::default()`, but both are: 11111111111111111111111111111111 +``` + +**Example:** + +```rust +invariant_ne!(owner, Pubkey::default()); +invariant_ne!(balance_after, balance_before, "balance should have changed"); +``` + +--- + +## `invariant_gt!` + +Checks that the first expression is strictly greater than the second. + +```rust +invariant_gt!(left, right); +invariant_gt!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `balance` > `0`, got: 0 vs 0 +``` + +**Example:** + +```rust +invariant_gt!(balance, 0); +invariant_gt!(supply_after, supply_before, "supply should increase after mint"); +``` + +--- + +## `invariant_gte!` + +Checks that the first expression is greater than or equal to the second. + +```rust +invariant_gte!(left, right); +invariant_gte!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `lamports` >= `rent_exempt`, got: 500 vs 1000 +``` + +**Example:** + +```rust +invariant_gte!(balance, minimum_balance); +invariant_gte!(lamports, rent_exempt, "account not rent-exempt"); +``` + +--- + +## `invariant_lt!` + +Checks that the first expression is strictly less than the second. + +```rust +invariant_lt!(left, right); +invariant_lt!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `fee` < `max_fee`, got: 500 vs 100 +``` + +**Example:** + +```rust +invariant_lt!(balance_after, balance_before); +invariant_lt!(fee, max_fee, "fee exceeds maximum"); +``` + +--- + +## `invariant_lte!` + +Checks that the first expression is less than or equal to the second. + +```rust +invariant_lte!(left, right); +invariant_lte!(left, right, "custom message"); +``` + +**Failure message (default):** + +``` +invariant violation: `total_supply` <= `max_supply`, got: 1000001 vs 1000000 +``` + +**Example:** + +```rust +invariant_lte!(withdrawal, balance); +invariant_lte!(total_supply, max_supply, "supply overflow"); +``` + +--- + + + diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 101314705..8439eef06 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -50,6 +50,8 @@ nav: - trident-api/token-2022.md - trident-api/vote-program.md - trident-api/stake-program.md + - Invariant Macros: + - trident-api/invariants.md - Address Storage: - trident-api/address-storage/index.md - Transaction Result: