Skip to content

Commit 4658897

Browse files
authored
Merge pull request #457 from Ackee-Blockchain/improve-invariants
Improve invariants
2 parents 138c34d + 7fba2d8 commit 4658897

14 files changed

Lines changed: 644 additions & 214 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ incremented upon a breaking change and the patch version will be incremented for
2525
**Changed**
2626

2727
- Allow initialization for Vanilla Solana projects with IDLs ([435](https://github.com/Ackee-Blockchain/trident/pull/435))
28+
- improve invariant handling and exit-code behavior in fuzzing ([457](https://github.com/Ackee-Blockchain/trident/pull/457))
2829

2930
## [0.12.0] - 2025-11-27
3031

crates/cli/src/command/fuzz.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(crate) enum FuzzCommand {
6363
long = "exit-code",
6464
required = false,
6565
value_name = "MODE",
66-
help = "Exit with non-zero code on failures. Modes: 'all' (any failure), 'invariants' (only fuzz test assertions), 'panics' (only program panics)."
66+
help = "Exit with non-zero code on failures. Modes: 'all' (any failure), 'invariants' (only custom invariant/assert failures)."
6767
)]
6868
exit_code: Option<ExitCodeMode>,
6969
#[arg(

crates/client/src/commander/mod.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ pub enum Error {
2727
BuildProgramsFailed,
2828
#[error("fuzzing failed")]
2929
FuzzingFailed,
30-
#[error("Fuzzing found failing invariants or unhandled panics")]
31-
FuzzingFailedInvariantOrPanic,
30+
#[error("Fuzzing failed due to exit-code policy (invariants/all)")]
31+
FuzzingFailedPolicy,
3232
#[error("Coverage error: {0}")]
3333
Coverage(#[from] crate::coverage::CoverageError),
3434
#[error("Cannot find the trident-tests directory in the current workspace")]
@@ -120,26 +120,31 @@ impl Commander {
120120
}
121121

122122
/// Manages a child process in an async context, specifically for monitoring fuzzing tasks.
123-
/// Waits for the process to exit or a Ctrl+C signal. Prints an error message if the process
124-
/// exits with an error, and sleeps briefly on Ctrl+C. Throws `Error::FuzzingFailed` on errors.
123+
/// Waits for the process to exit or a Ctrl+C signal.
124+
///
125+
/// Exit-code semantics:
126+
/// - `0`: success
127+
/// - `99`: policy failure from fuzz runner (only treated as error when policy is enabled)
128+
/// - other non-zero: runtime failure (always treated as error)
125129
///
126130
/// # Arguments
127131
/// * `child` - A mutable reference to a `Child` process.
132+
/// * `policy_enabled` - True when `--exit-code` policy is active.
128133
///
129134
/// # Errors
130-
/// * Throws `Error::FuzzingFailed` if waiting on the child process fails.
135+
/// * Throws `Error::FuzzingFailed` or `Error::FuzzingFailedPolicy` on failure.
131136
#[throws]
132-
async fn handle_child(child: &mut Child, with_exit_code: bool) {
137+
async fn handle_child(child: &mut Child, policy_enabled: bool) {
133138
tokio::select! {
134139
res = child.wait() =>
135140
match res {
136141
Ok(status) => match status.code() {
137142
Some(code) => {
138-
match (code, with_exit_code) {
139-
(0, _) => {} // fuzzing did not find any failing invariants or panics and we dont care about exit code
140-
(99, true) => throw!(Error::FuzzingFailedInvariantOrPanic), // fuzzing found failing invariants or panics and we care about exit code
141-
(99, false) => {} // fuzzing found failing invariants or panics and we dont care about exit code
142-
(_, _) => throw!(Error::FuzzingFailed), // fuzzing failed for some other reason so we care about exit code
143+
match (code, policy_enabled) {
144+
(0, _) => {}
145+
(99, true) => throw!(Error::FuzzingFailedPolicy),
146+
(99, false) => {}
147+
(_, _) => throw!(Error::FuzzingFailed),
143148
}
144149
}
145150
None => throw!(Error::FuzzingFailed),

crates/client/src/exit_code.rs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@ use std::str::FromStr;
33

44
/// Specifies which type of failures should cause a non-zero exit code.
55
///
6-
/// - `All`: Exit non-zero on any failure (program panics or invariant failures)
7-
/// - `Invariants`: Exit non-zero only on invariant/assert failures in fuzz tests
8-
/// - `Panics`: Exit non-zero only on program panics (program failed to complete)
6+
/// - `All`: Exit non-zero on any policy failure (program panics or custom invariant failures)
7+
/// - `Invariants`: Exit non-zero only on custom invariant failures in fuzz tests
98
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109
pub enum ExitCodeMode {
11-
/// Exit non-zero on any failure (default behavior when exit code is enabled)
10+
/// Exit non-zero on any policy failure (default behavior when exit code is enabled)
1211
#[default]
1312
All,
14-
/// Exit non-zero only on invariant/assert failures in fuzz tests
13+
/// Exit non-zero only on custom invariant failures in fuzz tests
1514
Invariants,
16-
/// Exit non-zero only on program panics (program failed to complete)
17-
Panics,
1815
}
1916

2017
impl ExitCodeMode {
@@ -23,19 +20,13 @@ impl ExitCodeMode {
2320
match self {
2421
ExitCodeMode::All => "all",
2522
ExitCodeMode::Invariants => "invariants",
26-
ExitCodeMode::Panics => "panics",
2723
}
2824
}
2925

3026
/// Check if this mode should trigger exit code for invariant failures
3127
pub fn triggers_on_invariants(&self) -> bool {
3228
matches!(self, ExitCodeMode::All | ExitCodeMode::Invariants)
3329
}
34-
35-
/// Check if this mode should trigger exit code for program panics
36-
pub fn triggers_on_panics(&self) -> bool {
37-
matches!(self, ExitCodeMode::All | ExitCodeMode::Panics)
38-
}
3930
}
4031

4132
impl fmt::Display for ExitCodeMode {
@@ -51,9 +42,8 @@ impl FromStr for ExitCodeMode {
5142
match s.to_lowercase().as_str() {
5243
"all" => Ok(ExitCodeMode::All),
5344
"invariants" => Ok(ExitCodeMode::Invariants),
54-
"panics" => Ok(ExitCodeMode::Panics),
5545
_ => Err(format!(
56-
"Invalid exit code mode '{}'. Valid values are: all, invariants, panics",
46+
"Invalid exit code mode '{}'. Valid values are: all, invariants",
5747
s
5848
)),
5949
}
@@ -71,23 +61,13 @@ mod tests {
7161
ExitCodeMode::from_str("invariants").unwrap(),
7262
ExitCodeMode::Invariants
7363
);
74-
assert_eq!(
75-
ExitCodeMode::from_str("panics").unwrap(),
76-
ExitCodeMode::Panics
77-
);
7864
assert_eq!(ExitCodeMode::from_str("ALL").unwrap(), ExitCodeMode::All);
7965
assert!(ExitCodeMode::from_str("invalid").is_err());
8066
}
8167

8268
#[test]
8369
fn test_triggers() {
8470
assert!(ExitCodeMode::All.triggers_on_invariants());
85-
assert!(ExitCodeMode::All.triggers_on_panics());
86-
8771
assert!(ExitCodeMode::Invariants.triggers_on_invariants());
88-
assert!(!ExitCodeMode::Invariants.triggers_on_panics());
89-
90-
assert!(!ExitCodeMode::Panics.triggers_on_invariants());
91-
assert!(ExitCodeMode::Panics.triggers_on_panics());
9272
}
9373
}

crates/client/src/utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,9 @@ mod tests {
538538
.await
539539
.unwrap();
540540

541+
// Keep TempDir alive across post-await checks to avoid eager drop in async state machine.
542+
assert!(temp_dir.path().exists());
543+
541544
// Verify no backup was created
542545
let backup_path = settings_path.with_extension("json.backup");
543546
assert!(!backup_path.exists());
@@ -574,6 +577,9 @@ mod tests {
574577
.await
575578
.unwrap();
576579

580+
// Keep TempDir alive across post-await checks to avoid eager drop in async state machine.
581+
assert!(temp_dir.path().exists());
582+
577583
// Verify no backup was created
578584
let backup_path = settings_path.with_extension("json.backup");
579585
assert!(!backup_path.exists());

crates/fuzz/src/invariant.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// Marker type for intentional invariant violations.
2+
/// Used to distinguish user-defined invariant failures from unexpected panics.
3+
#[derive(Debug)]
4+
pub struct InvariantViolation(pub String);
5+
6+
/// Checks a condition and panics with `InvariantViolation` if false.
7+
///
8+
/// Use this macro to mark assertions as intentional invariant checks.
9+
/// When an invariant fails, it will be counted and collected separately
10+
/// from unexpected panics (bugs in fuzz test code).
11+
///
12+
/// # Examples
13+
///
14+
/// ```ignore
15+
/// // Simple condition check
16+
/// invariant!(balance_after == balance_before - amount);
17+
/// invariant!(account.is_initialized);
18+
/// invariant!(owner != Pubkey::default());
19+
///
20+
/// // With custom message
21+
/// invariant!(balance > 0, "Balance must be positive");
22+
/// invariant!(a == b, "Expected {} but got {}", a, b);
23+
/// ```
24+
#[macro_export]
25+
macro_rules! invariant {
26+
($cond:expr) => {
27+
if !$cond {
28+
std::panic::panic_any($crate::invariant::InvariantViolation(
29+
format!("invariant violation: {}", stringify!($cond))
30+
));
31+
}
32+
};
33+
($cond:expr, $($msg:tt)*) => {
34+
if !$cond {
35+
std::panic::panic_any($crate::invariant::InvariantViolation(format!($($msg)*)));
36+
}
37+
};
38+
}

crates/fuzz/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod address_storage;
22
pub mod error;
3+
pub mod invariant;
34
pub mod trident;
45
pub mod trident_rng;
56

@@ -67,6 +68,10 @@ pub mod fuzzing {
6768
/// Error
6869
pub use super::error::*;
6970

71+
/// Invariant checking
72+
pub use super::invariant;
73+
pub use super::invariant::InvariantViolation;
74+
7075
/// Account discriminator trait
7176
pub use super::AccountDiscriminator;
7277

0 commit comments

Comments
 (0)