Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 1 addition & 1 deletion crates/codegen/src/yul/emitter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
31 changes: 25 additions & 6 deletions crates/codegen/src/yul/emitter/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ pub struct TestMetadata {
pub yul: String,
pub value_param_count: usize,
pub effect_param_count: usize,
pub expected_revert: Option<ExpectedRevert>,
}

/// 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<u8>),
// Selector([u8; 4]),
}

/// Output returned by `emit_test_module_yul`.
Expand Down Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -427,6 +439,7 @@ struct TestInfo {
object_name: String,
value_param_count: usize,
effect_param_count: usize,
expected_revert: Option<ExpectedRevert>,
}

/// Dependency set required to emit a single test object.
Expand All @@ -448,12 +461,15 @@ fn collect_test_infos(db: &dyn HirDb, functions: &[MirFunction<'_>]) -> Vec<Test
let MirFunctionOrigin::Hir(hir_func) = mir_func.origin else {
return None;
};
if !ItemKind::from(hir_func)
.attrs(db)
.is_some_and(|attrs| attrs.has_attr(db, "test"))
{
return None;
}
let attrs = ItemKind::from(hir_func).attrs(db)?;
let test_attr = attrs.get_attr(db, "test")?;

// Check for #[test(should_revert)]
let expected_revert = if test_attr.has_arg(db, "should_revert") {
Some(ExpectedRevert::Any)
} else {
None
};

let hir_name = hir_func
.name(db)
Expand All @@ -473,6 +489,7 @@ fn collect_test_infos(db: &dyn HirDb, functions: &[MirFunction<'_>]) -> Vec<Test
object_name: String::new(),
value_param_count,
effect_param_count,
expected_revert,
})
})
.collect()
Expand Down Expand Up @@ -1149,6 +1166,7 @@ mod tests {
object_name: String::new(),
value_param_count: 0,
effect_param_count: 0,
expected_revert: None,
},
TestInfo {
hir_name: "foo_bar".to_string(),
Expand All @@ -1157,6 +1175,7 @@ mod tests {
object_name: String::new(),
value_param_count: 0,
effect_param_count: 0,
expected_revert: None,
},
];

Expand Down
4 changes: 2 additions & 2 deletions crates/codegen/src/yul/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod errors;
mod state;

pub use emitter::{
EmitModuleError, TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout,
emit_test_module_yul, emit_test_module_yul_with_layout,
EmitModuleError, ExpectedRevert, TestMetadata, TestModuleOutput, emit_module_yul,
emit_module_yul_with_layout, emit_test_module_yul, emit_test_module_yul_with_layout,
};
pub use errors::YulError;
58 changes: 52 additions & 6 deletions crates/fe/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! executes them using revm.

use camino::Utf8PathBuf;
use codegen::{TestMetadata, emit_test_module_yul};
use codegen::{ExpectedRevert, TestMetadata, emit_test_module_yul};
use colored::Colorize;
use common::InputDb;
use contract_harness::{ExecutionOptions, RuntimeInstance};
Expand Down Expand Up @@ -298,20 +298,32 @@ fn compile_and_run_test(case: &TestMetadata, show_logs: bool) -> 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<String>) {
fn execute_test(
name: &str,
bytecode_hex: &str,
show_logs: bool,
expected_revert: Option<&ExpectedRevert>,
) -> (TestResult, Vec<String>) {
// Deploy the test contract
let mut instance = match RuntimeInstance::deploy(bytecode_hex) {
Ok(instance) => instance,
Expand All @@ -337,23 +349,57 @@ 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,
error_message: None,
},
logs,
),
Err(err) => (
// Normal test: execution reverted (failure)
(Err(err), None) => (
TestResult {
name: name.to_string(),
passed: false,
error_message: Some(format_harness_error(err)),
},
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))) => { ... }
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/fe/tests/fixtures/fe_test/should_revert.fe
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use std::evm::ops::revert

#[test(should_revert)]
fn should_revert() {
revert(0,0)
}
31 changes: 31 additions & 0 deletions crates/hir/src/core/hir_def/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down