Skip to content

Commit a6d6a3a

Browse files
authored
feat(cheatcodes): forge execution context check (#7377)
* feat(cheatcodes): forge execution context check * Add tests for test and snapshot contexts * Add isTestContext, isScriptContext cheatcodes * Add script dry run and broadcast tests * Proper enum in cheatcodes schema, alphabetical order * Single isContext cheatcode in env group, taking enum as param. remove context group * Changes after review: remove discriminant calls, use OnceLock * Review changes: tests should not be async * Review changes: implement PartialEq for ForgeContext, remove is_forge_context fn * Properly add new ForgeContext enum
1 parent a510447 commit a6d6a3a

File tree

9 files changed

+245
-1
lines changed

9 files changed

+245
-1
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cheatcodes/spec/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ impl Cheatcodes<'static> {
8888
enums: Cow::Owned(vec![
8989
Vm::CallerMode::ENUM.clone(),
9090
Vm::AccountAccessKind::ENUM.clone(),
91+
Vm::ForgeContext::ENUM.clone(),
9192
]),
9293
errors: Vm::VM_ERRORS.iter().map(|&x| x.clone()).collect(),
9394
events: Cow::Borrowed(&[]),

crates/cheatcodes/spec/src/vm.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![allow(missing_docs)]
44

55
use super::*;
6+
use crate::Vm::ForgeContext;
67
use alloy_sol_types::sol;
78
use foundry_macros::Cheatcode;
89

@@ -63,6 +64,28 @@ interface Vm {
6364
Extcodecopy,
6465
}
6566

67+
/// Forge execution contexts.
68+
enum ForgeContext {
69+
/// Test group execution context (test, coverage or snapshot).
70+
TestGroup,
71+
/// `forge test` execution context.
72+
Test,
73+
/// `forge coverage` execution context.
74+
Coverage,
75+
/// `forge snapshot` execution context.
76+
Snapshot,
77+
/// Script group execution context (dry run, broadcast or resume).
78+
ScriptGroup,
79+
/// `forge script` execution context.
80+
ScriptDryRun,
81+
/// `forge script --broadcast` execution context.
82+
ScriptBroadcast,
83+
/// `forge script --resume` execution context.
84+
ScriptResume,
85+
/// Unknown `forge` execution context.
86+
Unknown,
87+
}
88+
6689
/// An Ethereum log. Returned by `getRecordedLogs`.
6790
struct Log {
6891
/// The topics of the log, including the signature, if any.
@@ -1598,6 +1621,10 @@ interface Vm {
15981621
external view
15991622
returns (bytes[] memory value);
16001623

1624+
/// Returns true if `forge` command was executed in given context.
1625+
#[cheatcode(group = Environment)]
1626+
function isContext(ForgeContext context) external view returns (bool isContext);
1627+
16011628
// ======== Scripts ========
16021629

16031630
// -------- Broadcasting Transactions --------
@@ -2065,3 +2092,30 @@ interface Vm {
20652092
function toBase64URL(string calldata data) external pure returns (string memory);
20662093
}
20672094
}
2095+
2096+
impl PartialEq for ForgeContext {
2097+
// Handles test group case (any of test, coverage or snapshot)
2098+
// and script group case (any of dry run, broadcast or resume).
2099+
fn eq(&self, other: &Self) -> bool {
2100+
match (self, other) {
2101+
(_, &ForgeContext::TestGroup) => {
2102+
self == &ForgeContext::Test ||
2103+
self == &ForgeContext::Snapshot ||
2104+
self == &ForgeContext::Coverage
2105+
}
2106+
(_, &ForgeContext::ScriptGroup) => {
2107+
self == &ForgeContext::ScriptDryRun ||
2108+
self == &ForgeContext::ScriptBroadcast ||
2109+
self == &ForgeContext::ScriptResume
2110+
}
2111+
(&ForgeContext::Test, &ForgeContext::Test) |
2112+
(&ForgeContext::Snapshot, &ForgeContext::Snapshot) |
2113+
(&ForgeContext::Coverage, &ForgeContext::Coverage) |
2114+
(&ForgeContext::ScriptDryRun, &ForgeContext::ScriptDryRun) |
2115+
(&ForgeContext::ScriptBroadcast, &ForgeContext::ScriptBroadcast) |
2116+
(&ForgeContext::ScriptResume, &ForgeContext::ScriptResume) |
2117+
(&ForgeContext::Unknown, &ForgeContext::Unknown) => true,
2118+
_ => false,
2119+
}
2120+
}
2121+
}

crates/cheatcodes/src/env.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use crate::{string, Cheatcode, Cheatcodes, Error, Result, Vm::*};
44
use alloy_dyn_abi::DynSolType;
55
use alloy_primitives::Bytes;
66
use alloy_sol_types::SolValue;
7-
use std::env;
7+
use std::{env, sync::OnceLock};
8+
9+
/// Stores the forge execution context for the duration of the program.
10+
static FORGE_CONTEXT: OnceLock<ForgeContext> = OnceLock::new();
811

912
impl Cheatcode for setEnvCall {
1013
fn apply(&self, _state: &mut Cheatcodes) -> Result {
@@ -235,6 +238,19 @@ impl Cheatcode for envOr_13Call {
235238
}
236239
}
237240

241+
impl Cheatcode for isContextCall {
242+
fn apply(&self, _state: &mut Cheatcodes) -> Result {
243+
let Self { context } = self;
244+
Ok((FORGE_CONTEXT.get() == Some(context)).abi_encode())
245+
}
246+
}
247+
248+
/// Set `forge` command current execution context for the duration of the program.
249+
/// Execution context is immutable, subsequent calls of this function won't change the context.
250+
pub fn set_execution_context(context: ForgeContext) {
251+
let _ = FORGE_CONTEXT.set(context);
252+
}
253+
238254
fn env(key: &str, ty: &DynSolType) -> Result {
239255
get_env(key).and_then(|val| string::parse(&val, ty).map_err(map_env_err(key, &val)))
240256
}

crates/cheatcodes/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ mod test;
3434
mod toml;
3535
mod utils;
3636

37+
pub use env::set_execution_context;
3738
pub use script::ScriptWallets;
3839
pub use test::expect::ExpectedCallTracker;
40+
pub use Vm::ForgeContext;
3941

4042
/// Cheatcode implementation.
4143
pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode {

crates/forge/bin/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use clap::{CommandFactory, Parser};
55
use clap_complete::generate;
66
use eyre::Result;
77
use foundry_cli::{handler, utils};
8+
use foundry_evm::inspectors::cheatcodes::{set_execution_context, ForgeContext};
89

910
mod cmd;
1011
use cmd::{cache::CacheSubcommands, generate::GenerateSubcommands, watch};
@@ -23,6 +24,8 @@ fn main() -> Result<()> {
2324
utils::enable_paint();
2425

2526
let opts = Forge::parse();
27+
init_execution_context(&opts.cmd);
28+
2629
match opts.cmd {
2730
ForgeSubcommand::Test(cmd) => {
2831
if cmd.is_watch() {
@@ -107,3 +110,25 @@ fn main() -> Result<()> {
107110
},
108111
}
109112
}
113+
114+
/// Set the program execution context based on `forge` subcommand used.
115+
/// The execution context can be set only once per program, and it can be checked by using
116+
/// cheatcodes.
117+
fn init_execution_context(subcommand: &ForgeSubcommand) {
118+
let context = match subcommand {
119+
ForgeSubcommand::Test(_) => ForgeContext::Test,
120+
ForgeSubcommand::Coverage(_) => ForgeContext::Coverage,
121+
ForgeSubcommand::Snapshot(_) => ForgeContext::Snapshot,
122+
ForgeSubcommand::Script(cmd) => {
123+
if cmd.broadcast {
124+
ForgeContext::ScriptBroadcast
125+
} else if cmd.resume {
126+
ForgeContext::ScriptResume
127+
} else {
128+
ForgeContext::ScriptDryRun
129+
}
130+
}
131+
_ => ForgeContext::Unknown,
132+
};
133+
set_execution_context(context);
134+
}

crates/forge/tests/cli/context.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//! Contains tests for checking forge execution context cheatcodes
2+
const FORGE_TEST_CONTEXT_CONTRACT: &str = r#"
3+
import "./test.sol";
4+
interface Vm {
5+
enum ForgeContext { TestGroup, Test, Coverage, Snapshot, ScriptGroup, ScriptDryRun, ScriptBroadcast, ScriptResume, Unknown }
6+
function isContext(ForgeContext context) external view returns (bool isContext);
7+
}
8+
9+
contract ForgeContextTest is DSTest {
10+
Vm constant vm = Vm(HEVM_ADDRESS);
11+
12+
function testForgeTestContext() external view {
13+
require(vm.isContext(Vm.ForgeContext.TestGroup) && !vm.isContext(Vm.ForgeContext.ScriptGroup), "wrong context");
14+
require(vm.isContext(Vm.ForgeContext.Test), "wrong context");
15+
require(!vm.isContext(Vm.ForgeContext.Coverage), "wrong context");
16+
require(!vm.isContext(Vm.ForgeContext.Snapshot), "wrong context");
17+
}
18+
function testForgeSnapshotContext() external view {
19+
require(vm.isContext(Vm.ForgeContext.TestGroup) && !vm.isContext(Vm.ForgeContext.ScriptGroup), "wrong context");
20+
require(vm.isContext(Vm.ForgeContext.Snapshot), "wrong context");
21+
require(!vm.isContext(Vm.ForgeContext.Test), "wrong context");
22+
require(!vm.isContext(Vm.ForgeContext.Coverage), "wrong context");
23+
}
24+
function testForgeCoverageContext() external view {
25+
require(vm.isContext(Vm.ForgeContext.TestGroup) && !vm.isContext(Vm.ForgeContext.ScriptGroup), "wrong context");
26+
require(vm.isContext(Vm.ForgeContext.Coverage), "wrong context");
27+
require(!vm.isContext(Vm.ForgeContext.Test), "wrong context");
28+
require(!vm.isContext(Vm.ForgeContext.Snapshot), "wrong context");
29+
}
30+
31+
function runDryRun() external view {
32+
require(vm.isContext(Vm.ForgeContext.ScriptGroup) && !vm.isContext(Vm.ForgeContext.TestGroup), "wrong context");
33+
require(vm.isContext(Vm.ForgeContext.ScriptDryRun), "wrong context");
34+
require(!vm.isContext(Vm.ForgeContext.ScriptBroadcast), "wrong context");
35+
require(!vm.isContext(Vm.ForgeContext.ScriptResume), "wrong context");
36+
}
37+
function runBroadcast() external view {
38+
require(vm.isContext(Vm.ForgeContext.ScriptGroup) && !vm.isContext(Vm.ForgeContext.TestGroup), "wrong context");
39+
require(vm.isContext(Vm.ForgeContext.ScriptBroadcast), "wrong context");
40+
require(!vm.isContext(Vm.ForgeContext.ScriptDryRun), "wrong context");
41+
require(!vm.isContext(Vm.ForgeContext.ScriptResume), "wrong context");
42+
}
43+
}
44+
"#;
45+
46+
// tests that context properly set for `forge test` command
47+
forgetest!(can_set_forge_test_standard_context, |prj, cmd| {
48+
prj.insert_ds_test();
49+
prj.add_source("ForgeContextTest.t.sol", FORGE_TEST_CONTEXT_CONTRACT).unwrap();
50+
cmd.args(["test", "--match-test", "testForgeTestContext"]).assert_success();
51+
});
52+
53+
// tests that context properly set for `forge snapshot` command
54+
forgetest!(can_set_forge_test_snapshot_context, |prj, cmd| {
55+
prj.insert_ds_test();
56+
prj.add_source("ForgeContextTest.t.sol", FORGE_TEST_CONTEXT_CONTRACT).unwrap();
57+
cmd.args(["snapshot", "--match-test", "testForgeSnapshotContext"]).assert_success();
58+
});
59+
60+
// tests that context properly set for `forge coverage` command
61+
forgetest!(can_set_forge_test_coverage_context, |prj, cmd| {
62+
prj.insert_ds_test();
63+
prj.add_source("ForgeContextTest.t.sol", FORGE_TEST_CONTEXT_CONTRACT).unwrap();
64+
cmd.args(["coverage", "--match-test", "testForgeCoverageContext"]).assert_success();
65+
});
66+
67+
// tests that context properly set for `forge script` command
68+
forgetest!(can_set_forge_script_dry_run_context, |prj, cmd| {
69+
prj.insert_ds_test();
70+
let script =
71+
prj.add_source("ForgeScriptContextTest.s.sol", FORGE_TEST_CONTEXT_CONTRACT).unwrap();
72+
cmd.arg("script").arg(script).args(["--sig", "runDryRun()"]).assert_success();
73+
});
74+
75+
// tests that context properly set for `forge script --broadcast` command
76+
forgetest!(can_set_forge_script_broadcast_context, |prj, cmd| {
77+
prj.insert_ds_test();
78+
let script =
79+
prj.add_source("ForgeScriptContextTest.s.sol", FORGE_TEST_CONTEXT_CONTRACT).unwrap();
80+
cmd.arg("script").arg(script).args(["--broadcast", "--sig", "runBroadcast()"]).assert_success();
81+
});

crates/forge/tests/cli/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod build;
88
mod cache;
99
mod cmd;
1010
mod config;
11+
mod context;
1112
mod coverage;
1213
mod create;
1314
mod debug;

testdata/cheats/Vm.sol

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)