Skip to content

Commit d3bec03

Browse files
committed
Implement should_revert for in test runner
1 parent 84a9cc5 commit d3bec03

File tree

9 files changed

+150
-102
lines changed

9 files changed

+150
-102
lines changed

crates/codegen/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod yul;
22

33
pub use yul::{
4-
EmitModuleError, TestMetadata, TestModuleOutput, YulError, emit_module_yul,
4+
EmitModuleError, ExpectedRevert, TestMetadata, TestModuleOutput, YulError, emit_module_yul,
55
emit_module_yul_with_layout, emit_test_module_yul, emit_test_module_yul_with_layout,
66
};

crates/codegen/src/yul/emitter/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt;
33
use crate::yul::errors::YulError;
44

55
pub use module::{
6-
TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout,
6+
ExpectedRevert, TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout,
77
emit_test_module_yul, emit_test_module_yul_with_layout,
88
};
99

crates/codegen/src/yul/emitter/module.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ pub struct TestMetadata {
3636
pub yul: String,
3737
pub value_param_count: usize,
3838
pub effect_param_count: usize,
39+
pub expected_revert: Option<ExpectedRevert>,
40+
}
41+
42+
/// Describes the expected revert behavior for a test.
43+
#[derive(Debug, Clone, PartialEq, Eq)]
44+
pub enum ExpectedRevert {
45+
/// Test should revert with any data.
46+
Any,
47+
// Future phases:
48+
// ExactData(Vec<u8>),
49+
// Selector([u8; 4]),
3950
}
4051

4152
/// Output returned by `emit_test_module_yul`.
@@ -333,6 +344,7 @@ pub fn emit_test_module_yul_with_layout(
333344
yul,
334345
value_param_count: test.value_param_count,
335346
effect_param_count: test.effect_param_count,
347+
expected_revert: test.expected_revert,
336348
});
337349
}
338350

@@ -427,6 +439,7 @@ struct TestInfo {
427439
object_name: String,
428440
value_param_count: usize,
429441
effect_param_count: usize,
442+
expected_revert: Option<ExpectedRevert>,
430443
}
431444

432445
/// Dependency set required to emit a single test object.
@@ -448,12 +461,15 @@ fn collect_test_infos(db: &dyn HirDb, functions: &[MirFunction<'_>]) -> Vec<Test
448461
let MirFunctionOrigin::Hir(hir_func) = mir_func.origin else {
449462
return None;
450463
};
451-
if !ItemKind::from(hir_func)
452-
.attrs(db)
453-
.is_some_and(|attrs| attrs.has_attr(db, "test"))
454-
{
455-
return None;
456-
}
464+
let attrs = ItemKind::from(hir_func).attrs(db)?;
465+
let test_attr = attrs.get_attr(db, "test")?;
466+
467+
// Check for #[test(should_revert)]
468+
let expected_revert = if test_attr.has_arg(db, "should_revert") {
469+
Some(ExpectedRevert::Any)
470+
} else {
471+
None
472+
};
457473

458474
let hir_name = hir_func
459475
.name(db)
@@ -473,6 +489,7 @@ fn collect_test_infos(db: &dyn HirDb, functions: &[MirFunction<'_>]) -> Vec<Test
473489
object_name: String::new(),
474490
value_param_count,
475491
effect_param_count,
492+
expected_revert,
476493
})
477494
})
478495
.collect()
@@ -1149,6 +1166,7 @@ mod tests {
11491166
object_name: String::new(),
11501167
value_param_count: 0,
11511168
effect_param_count: 0,
1169+
expected_revert: None,
11521170
},
11531171
TestInfo {
11541172
hir_name: "foo_bar".to_string(),
@@ -1157,6 +1175,7 @@ mod tests {
11571175
object_name: String::new(),
11581176
value_param_count: 0,
11591177
effect_param_count: 0,
1178+
expected_revert: None,
11601179
},
11611180
];
11621181

crates/codegen/src/yul/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod errors;
44
mod state;
55

66
pub use emitter::{
7-
EmitModuleError, TestMetadata, TestModuleOutput, emit_module_yul, emit_module_yul_with_layout,
8-
emit_test_module_yul, emit_test_module_yul_with_layout,
7+
EmitModuleError, ExpectedRevert, TestMetadata, TestModuleOutput, emit_module_yul,
8+
emit_module_yul_with_layout, emit_test_module_yul, emit_test_module_yul_with_layout,
99
};
1010
pub use errors::YulError;

crates/fe/src/test.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! executes them using revm.
55
66
use camino::Utf8PathBuf;
7-
use codegen::{TestMetadata, emit_test_module_yul};
7+
use codegen::{ExpectedRevert, TestMetadata, emit_test_module_yul};
88
use colored::Colorize;
99
use common::InputDb;
1010
use contract_harness::{ExecutionOptions, RuntimeInstance};
@@ -298,20 +298,32 @@ fn compile_and_run_test(case: &TestMetadata, show_logs: bool) -> TestOutcome {
298298
};
299299

300300
// Execute the test bytecode in revm
301-
let (result, logs) = execute_test(&case.display_name, &bytecode, show_logs);
301+
let (result, logs) = execute_test(
302+
&case.display_name,
303+
&bytecode,
304+
show_logs,
305+
case.expected_revert.as_ref(),
306+
);
302307
TestOutcome { result, logs }
303308
}
304309

305310
/// Deploys and executes compiled test bytecode in revm.
306311
///
307312
/// The test passes if the function returns normally, fails if it reverts.
313+
/// When `expected_revert` is set, the logic is inverted: the test passes if it reverts.
308314
///
309315
/// * `name` - Display name used for reporting.
310316
/// * `bytecode_hex` - Hex-encoded init bytecode for the test object.
311317
/// * `show_logs` - Whether to execute with log collection enabled.
318+
/// * `expected_revert` - If set, the test is expected to revert.
312319
///
313320
/// Returns the test result and any emitted logs.
314-
fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult, Vec<String>) {
321+
fn execute_test(
322+
name: &str,
323+
bytecode_hex: &str,
324+
show_logs: bool,
325+
expected_revert: Option<&ExpectedRevert>,
326+
) -> (TestResult, Vec<String>) {
315327
// Deploy the test contract
316328
let mut instance = match RuntimeInstance::deploy(bytecode_hex) {
317329
Ok(instance) => instance,
@@ -337,23 +349,45 @@ fn execute_test(name: &str, bytecode_hex: &str, show_logs: bool) -> (TestResult,
337349
instance.call_raw(&[], options).map(|_| Vec::new())
338350
};
339351

340-
match call_result {
341-
Ok(logs) => (
352+
match (call_result, expected_revert) {
353+
// Normal test: execution succeeded
354+
(Ok(logs), None) => (
342355
TestResult {
343356
name: name.to_string(),
344357
passed: true,
345358
error_message: None,
346359
},
347360
logs,
348361
),
349-
Err(err) => (
362+
// Normal test: execution reverted (failure)
363+
(Err(err), None) => (
350364
TestResult {
351365
name: name.to_string(),
352366
passed: false,
353367
error_message: Some(format_harness_error(err)),
354368
},
355369
Vec::new(),
356370
),
371+
// Expected revert: execution succeeded (failure - should have reverted)
372+
(Ok(_), Some(_)) => (
373+
TestResult {
374+
name: name.to_string(),
375+
passed: false,
376+
error_message: Some("Expected test to revert, but it succeeded".to_string()),
377+
},
378+
Vec::new(),
379+
),
380+
// Expected revert: execution reverted (success)
381+
(Err(_err), Some(ExpectedRevert::Any)) => (
382+
TestResult {
383+
name: name.to_string(),
384+
passed: true,
385+
error_message: None,
386+
},
387+
Vec::new(),
388+
),
389+
// Future: match specific revert data
390+
// (Err(HarnessError::Revert(data)), Some(ExpectedRevert::ExactData(expected))) => { ... }
357391
}
358392
}
359393

crates/fe/tests/fixtures/fe_test/checked_arithmetic_methods.fe

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,53 @@ fn test_wrapping_and_saturating_u8() {
2828
}
2929

3030
fn as_u8(_ x: u8) -> u8 { x }
31+
32+
// Tests that verify overflow/underflow causes reverts
33+
34+
#[test(should_revert)]
35+
fn test_add_overflow_u8() {
36+
let x: u8 = 255
37+
let y: u8 = x + 1
38+
}
39+
40+
#[test(should_revert)]
41+
fn test_aug_assign_overflow_u8() {
42+
let mut x: u8 = 255
43+
x += 1
44+
}
45+
46+
#[test(should_revert)]
47+
fn test_div_by_zero_u8() {
48+
let x: u8 = 1
49+
let y: u8 = x / 0
50+
}
51+
52+
#[test(should_revert)]
53+
fn test_rem_by_zero_u8() {
54+
let x: u8 = 1
55+
let y: u8 = x % 0
56+
}
57+
58+
#[test(should_revert)]
59+
fn test_pow_overflow_u8() {
60+
let x: u8 = 2
61+
let y: u8 = x ** 8
62+
}
63+
64+
#[test(should_revert)]
65+
fn test_neg_overflow_i8() {
66+
let x: i8 = -128
67+
let y: i8 = -x
68+
}
69+
70+
#[test(should_revert)]
71+
fn test_div_overflow_i8() {
72+
let x: i8 = -128
73+
let y: i8 = x / -1
74+
}
75+
76+
#[test(should_revert)]
77+
fn test_pow_negative_exp_i8() {
78+
let x: i8 = 2
79+
let y: i8 = x ** -1
80+
}

crates/fe/tests/fixtures/fe_test_runner/checked_arithmetic_reverts.fe

Lines changed: 0 additions & 47 deletions
This file was deleted.

crates/fe/tests/fixtures/fe_test_runner/checked_arithmetic_reverts.snap

Lines changed: 0 additions & 39 deletions
This file was deleted.

crates/hir/src/core/hir_def/attr.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,37 @@ impl<'db> AttrListId<'db> {
2525
}
2626
})
2727
}
28+
29+
/// Returns the attribute with the given name, if present.
30+
pub fn get_attr(self, db: &'db dyn HirDb, name: &str) -> Option<&'db NormalAttr<'db>> {
31+
self.data(db).iter().find_map(|attr| {
32+
if let Attr::Normal(normal_attr) = attr
33+
&& let Some(path) = normal_attr.path.to_opt()
34+
&& let Some(ident) = path.as_ident(db)
35+
&& ident.data(db) == name
36+
{
37+
Some(normal_attr)
38+
} else {
39+
None
40+
}
41+
})
42+
}
43+
}
44+
45+
impl<'db> NormalAttr<'db> {
46+
/// Returns true if this attribute has an argument with the given key (no value).
47+
///
48+
/// For example, `#[test(should_revert)]` has the argument `should_revert`.
49+
pub fn has_arg(&self, db: &'db dyn HirDb, key: &str) -> bool {
50+
self.args.iter().any(|arg| {
51+
arg.value.is_none()
52+
&& arg
53+
.key
54+
.to_opt()
55+
.and_then(|p| p.as_ident(db))
56+
.is_some_and(|ident| ident.data(db) == key)
57+
})
58+
}
2859
}
2960

3061
#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::From)]

0 commit comments

Comments
 (0)