diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 0ed029fa69..fe671a40f7 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -1,6 +1,6 @@ mod yul; pub use yul::{ - EmitModuleError, TestMetadata, TestModuleOutput, YulError, emit_module_yul, + EmitModuleError, ExpectedRevert, TestMetadata, TestModuleOutput, YulError, emit_module_yul, emit_module_yul_with_layout, emit_test_module_yul, emit_test_module_yul_with_layout, }; diff --git a/crates/codegen/src/yul/emitter/mod.rs b/crates/codegen/src/yul/emitter/mod.rs index e854c74b4c..0b279b1b74 100644 --- a/crates/codegen/src/yul/emitter/mod.rs +++ b/crates/codegen/src/yul/emitter/mod.rs @@ -3,7 +3,7 @@ use std::fmt; use crate::yul::errors::YulError; pub use module::{ - TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout, + ExpectedRevert, TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout, emit_test_module_yul, emit_test_module_yul_with_layout, }; diff --git a/crates/codegen/src/yul/emitter/module.rs b/crates/codegen/src/yul/emitter/module.rs index a7e7826b21..cfb0e408b6 100644 --- a/crates/codegen/src/yul/emitter/module.rs +++ b/crates/codegen/src/yul/emitter/module.rs @@ -36,6 +36,17 @@ pub struct TestMetadata { pub yul: String, pub value_param_count: usize, pub effect_param_count: usize, + pub expected_revert: Option, +} + +/// Describes the expected revert behavior for a test. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExpectedRevert { + /// Test should revert with any data. + Any, + // Future phases: + // ExactData(Vec), + // Selector([u8; 4]), } /// Output returned by `emit_test_module_yul`. @@ -333,6 +344,7 @@ pub fn emit_test_module_yul_with_layout( yul, value_param_count: test.value_param_count, effect_param_count: test.effect_param_count, + expected_revert: test.expected_revert, }); } @@ -427,6 +439,7 @@ struct TestInfo { object_name: String, value_param_count: usize, effect_param_count: usize, + expected_revert: Option, } /// Dependency set required to emit a single test object. @@ -448,12 +461,15 @@ fn collect_test_infos(db: &dyn HirDb, functions: &[MirFunction<'_>]) -> Vec]) -> Vec TestOutcome { }; // Execute the test bytecode in revm - let (result, logs) = execute_test(&case.display_name, &bytecode, show_logs); + let (result, logs) = execute_test( + &case.display_name, + &bytecode, + show_logs, + case.expected_revert.as_ref(), + ); TestOutcome { result, logs } } /// Deploys and executes compiled test bytecode in revm. /// /// The test passes if the function returns normally, fails if it reverts. +/// When `expected_revert` is set, the logic is inverted: the test passes if it reverts. /// /// * `name` - Display name used for reporting. /// * `bytecode_hex` - Hex-encoded init bytecode for the test object. /// * `show_logs` - Whether to execute with log collection enabled. +/// * `expected_revert` - If set, the test is expected to revert. /// /// Returns the test result and any emitted logs. -fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult, Vec) { +fn execute_test( + name: &str, + bytecode_hex: &str, + show_logs: bool, + expected_revert: Option<&ExpectedRevert>, +) -> (TestResult, Vec) { // Deploy the test contract let mut instance = match RuntimeInstance::deploy(bytecode_hex) { Ok(instance) => instance, @@ -337,8 +349,9 @@ fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult, instance.call_raw(&[], options).map(|_| Vec::new()) }; - match call_result { - Ok(logs) => ( + match (call_result, expected_revert) { + // Normal test: execution succeeded + (Ok(logs), None) => ( TestResult { name: name.to_string(), passed: true, @@ -346,7 +359,8 @@ fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult, }, logs, ), - Err(err) => ( + // Normal test: execution reverted (failure) + (Err(err), None) => ( TestResult { name: name.to_string(), passed: false, @@ -354,6 +368,38 @@ fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult, }, Vec::new(), ), + // Expected revert: execution succeeded (failure - should have reverted) + (Ok(_), Some(_)) => ( + TestResult { + name: name.to_string(), + passed: false, + error_message: Some("Expected test to revert, but it succeeded".to_string()), + }, + Vec::new(), + ), + // Expected revert: execution reverted (success) + (Err(contract_harness::HarnessError::Revert(_)), Some(ExpectedRevert::Any)) => ( + TestResult { + name: name.to_string(), + passed: true, + error_message: None, + }, + Vec::new(), + ), + // Expected revert: execution failed for a different reason (failure) + (Err(err), Some(ExpectedRevert::Any)) => ( + TestResult { + name: name.to_string(), + passed: false, + error_message: Some(format!( + "Expected test to revert, but it failed with: {}", + format_harness_error(err) + )), + }, + Vec::new(), + ), + // Future: match specific revert data + // (Err(HarnessError::Revert(data)), Some(ExpectedRevert::ExactData(expected))) => { ... } } } diff --git a/crates/fe/tests/fixtures/fe_test/should_revert.fe b/crates/fe/tests/fixtures/fe_test/should_revert.fe new file mode 100644 index 0000000000..e421287dbc --- /dev/null +++ b/crates/fe/tests/fixtures/fe_test/should_revert.fe @@ -0,0 +1,6 @@ +use std::evm::ops::revert + +#[test(should_revert)] +fn should_revert() { + revert(0,0) +} \ No newline at end of file diff --git a/crates/hir/src/core/hir_def/attr.rs b/crates/hir/src/core/hir_def/attr.rs index 097d2da605..e3ed78bda6 100644 --- a/crates/hir/src/core/hir_def/attr.rs +++ b/crates/hir/src/core/hir_def/attr.rs @@ -25,6 +25,37 @@ impl<'db> AttrListId<'db> { } }) } + + /// Returns the attribute with the given name, if present. + pub fn get_attr(self, db: &'db dyn HirDb, name: &str) -> Option<&'db NormalAttr<'db>> { + self.data(db).iter().find_map(|attr| { + if let Attr::Normal(normal_attr) = attr + && let Some(path) = normal_attr.path.to_opt() + && let Some(ident) = path.as_ident(db) + && ident.data(db) == name + { + Some(normal_attr) + } else { + None + } + }) + } +} + +impl<'db> NormalAttr<'db> { + /// Returns true if this attribute has an argument with the given key (no value). + /// + /// For example, `#[test(should_revert)]` has the argument `should_revert`. + pub fn has_arg(&self, db: &'db dyn HirDb, key: &str) -> bool { + self.args.iter().any(|arg| { + arg.value.is_none() + && arg + .key + .to_opt() + .and_then(|p| p.as_ident(db)) + .is_some_and(|ident| ident.data(db) == key) + }) + } } #[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]