Skip to content

Commit 2c5564b

Browse files
authored
Update all the E2E should_fail tests to verify their output. (#2082)
Using the `FileCheck` crate it can now pattern match against the compiler output to be sure the errors and/or warnings are exactly what we expect.
1 parent 289f301 commit 2c5564b

File tree

95 files changed

+627
-33
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+627
-33
lines changed

Cargo.lock

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

test/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ publish = false
77

88
[dependencies]
99
anyhow = "1.0.41"
10+
filecheck = "0.5"
1011
forc = { path = "../forc", features = ["test"], default-features = false }
1112
forc-util = { path = "../forc-util" }
1213
fuel-asm = "0.5"
1314
fuel-tx = "0.12"
1415
fuel-vm = { version = "0.11", features = ["random"] }
16+
gag = "1.0"
1517
rand = "0.8"
1618
regex = "1"
1719
serde_json = "1.0.73"

test/src/e2e_vm_tests/harness.rs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,51 @@ pub(crate) fn runs_in_vm(file_name: &str, locked: bool) -> ProgramState {
9898
*i.transact(tx_to_test).unwrap().state()
9999
}
100100

101-
/// Panics if code _does_ compile, used for test cases where the source
102-
/// code should have been rejected by the compiler.
103-
pub(crate) fn does_not_compile(file_name: &str, locked: bool) {
104-
assert!(
105-
compile_to_bytes(file_name, locked).is_err(),
106-
"{} should not have compiled.",
107-
file_name,
108-
)
101+
/// Returns Err(()) if code _does_ compile, used for test cases where the source
102+
/// code should have been rejected by the compiler. When it fails to compile the
103+
/// captured stdout is returned.
104+
pub(crate) fn does_not_compile(file_name: &str, locked: bool) -> Result<String, ()> {
105+
use std::io::Read;
106+
107+
tracing::info!(" Compiling {}", file_name);
108+
109+
// Capture stdout to a buffer, compile the test and save stdout to a string.
110+
let mut buf = gag::BufferRedirect::stdout().unwrap();
111+
let result = compile_to_bytes_verbose(file_name, locked, true);
112+
let mut output = String::new();
113+
buf.read_to_string(&mut output).unwrap();
114+
drop(buf);
115+
116+
// If verbosity is requested then print it out.
117+
if get_test_config_from_env() {
118+
tracing::info!("{output}");
119+
}
120+
121+
// Invert the result; if it succeeds then return an Err.
122+
match result {
123+
Ok(_) => Err(()),
124+
Err(e) => {
125+
// Capture the result of the compilation (i.e., any errors Forc produces) and append to
126+
// the stdout from the compiler.
127+
output.push_str(&format!("\n{e}"));
128+
Ok(output)
129+
}
130+
}
109131
}
110132

111133
/// Returns `true` if a file compiled without any errors or warnings,
112134
/// and `false` if it did not.
113135
pub(crate) fn compile_to_bytes(file_name: &str, locked: bool) -> Result<Vec<u8>> {
136+
compile_to_bytes_verbose(file_name, locked, get_test_config_from_env())
137+
}
138+
139+
pub(crate) fn compile_to_bytes_verbose(
140+
file_name: &str,
141+
locked: bool,
142+
verbose: bool,
143+
) -> Result<Vec<u8>> {
114144
tracing::info!(" Compiling {}", file_name);
115145
let manifest_dir = env!("CARGO_MANIFEST_DIR");
116-
let verbose = get_test_config_from_env();
117146
forc_build::build(BuildCommand {
118147
path: Some(format!(
119148
"{}/src/e2e_vm_tests/test_programs/{}",

test/src/e2e_vm_tests/mod.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{
1010
path::{Path, PathBuf},
1111
};
1212

13-
#[derive(Debug)]
13+
#[derive(PartialEq)]
1414
enum TestCategory {
1515
Compiles,
1616
FailsToCompile,
@@ -27,13 +27,13 @@ enum TestResult {
2727
Revert(u64),
2828
}
2929

30-
#[derive(Debug)]
3130
struct TestDescription {
3231
name: String,
3332
category: TestCategory,
3433
expected_result: Option<TestResult>,
3534
contract_paths: Vec<String>,
3635
validate_abi: bool,
36+
checker: filecheck::Checker,
3737
}
3838

3939
pub fn run(locked: bool, filter_regex: Option<regex::Regex>) {
@@ -55,6 +55,7 @@ pub fn run(locked: bool, filter_regex: Option<regex::Regex>) {
5555
expected_result,
5656
contract_paths,
5757
validate_abi,
58+
checker,
5859
} in configured_tests
5960
{
6061
if !filter_regex
@@ -91,7 +92,20 @@ pub fn run(locked: bool, filter_regex: Option<regex::Regex>) {
9192
}
9293

9394
TestCategory::FailsToCompile => {
94-
crate::e2e_vm_tests::harness::does_not_compile(&name, locked);
95+
match crate::e2e_vm_tests::harness::does_not_compile(&name, locked) {
96+
Ok(output) => match checker.explain(&output, filecheck::NO_VARIABLES) {
97+
Ok((success, report)) if !success => {
98+
panic!("For {name}:\nFilecheck failed:\n{report}");
99+
}
100+
Err(e) => {
101+
panic!("For {name}:\nFilecheck directive error: {e}");
102+
}
103+
_ => (),
104+
},
105+
Err(_) => {
106+
panic!("For {name}:\nFailing test did not fail.");
107+
}
108+
}
95109
number_of_tests_executed += 1;
96110
}
97111

@@ -191,12 +205,19 @@ fn discover_test_configs() -> Result<Vec<TestDescription>, String> {
191205
}
192206

193207
fn parse_test_toml(path: &Path) -> Result<TestDescription, String> {
194-
let toml_content = std::fs::read_to_string(path)
208+
let (toml_content, checker) = std::fs::read_to_string(path)
195209
.map_err(|e| e.to_string())
196210
.and_then(|toml_content_str| {
197-
toml_content_str
198-
.parse::<toml::Value>()
211+
// Parse the file for FileCheck directives and_then parse the file into TOML.
212+
filecheck::CheckerBuilder::new()
213+
.text(&toml_content_str)
199214
.map_err(|e| e.to_string())
215+
.and_then(|checker| {
216+
toml_content_str
217+
.parse::<toml::Value>()
218+
.map_err(|e| e.to_string())
219+
.map(|toml_content| (toml_content, checker.finish()))
220+
})
200221
})
201222
.map_err(|e| format!("Failed to parse: {e}"))?;
202223

@@ -219,6 +240,11 @@ fn parse_test_toml(path: &Path) -> Result<TestDescription, String> {
219240
Some(other) => Err(format!("Unknown category '{}'.", other,)),
220241
})?;
221242

243+
// Abort early if we find a FailsToCompile test without any Checker directives.
244+
if category == TestCategory::FailsToCompile && checker.is_empty() {
245+
return Err("'fail' tests must contain some FileCheck verification directives.".to_owned());
246+
}
247+
222248
let expected_result = match &category {
223249
TestCategory::Runs | TestCategory::RunsWithContract => {
224250
Some(get_expected_result(&toml_content)?)
@@ -268,6 +294,7 @@ fn parse_test_toml(path: &Path) -> Result<TestDescription, String> {
268294
expected_result,
269295
contract_paths,
270296
validate_abi,
297+
checker,
271298
})
272299
}
273300

test/src/e2e_vm_tests/test_programs/README.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
Each of the tests in this suite are controlled by a TOML descriptor file which describes how the
44
test should be run and what result to expect if any.
55

6-
## `test.toml`
6+
## test.toml
77

88
To add a new test to the E2E suite place a `test.toml` file at the root of the test Forc package,
99
i.e., next to the `Forc.toml` file. This file may contain a few basic fields.
1010

11-
## `category`
11+
## category
1212

1313
The `category` field is mandatory and must be one of the following strings:
1414

@@ -18,7 +18,7 @@ The `category` field is mandatory and must be one of the following strings:
1818
* `"fail"` - The test is expected to fail to compile.
1919
* `"disabled"` - The test is disabled.
2020

21-
## `expected_result`
21+
## expected_result
2222

2323
The `expected_result` field is mandatory for `"run"` and `"run_on_node"` tests. It is a table with
2424
two fields, `action` and `value`.
@@ -35,7 +35,7 @@ it must be an integer.
3535

3636
For `"return_data"` actions it must be an array of byte values, each an integer between 0 and 255.
3737

38-
## `contracts`
38+
## contracts
3939

4040
Tests in the `"run_on_node"` category will usually specify one or more contracts which must be
4141
deployed to the node prior to deploying and running the test code. These are specified with the
@@ -45,11 +45,31 @@ It must be an array of strings each containing only the path to the Forc project
4545
be compiled and deployed. It is important that these paths remain relative to the
4646
`test/src/e2e_vm_tests/test_programs` directory.
4747

48-
## `validate_abi`
48+
## validate_abi
4949

5050
Some tests also require their ABI is verified. To indicate this the `validate_abi` field may be
5151
specified, as a boolean value.
5252

53+
# FileCheck for 'fail' tests
54+
55+
The tests in the `fail` category _must_ employ verification using pattern matching via the [FileCheck](https://docs.rs/filecheck/latest/filecheck/)
56+
crate. The checker directives are specified in comments (lines beginning with `#`) in the `test.toml`
57+
file.
58+
59+
Typically this is as simple as just adding a `# check: ...` line to the line specifying the full
60+
error or warning message expected from compiling the test. `FileCheck` also has other directives for
61+
fancier pattern matching, as specified in the [FileCheck docs](https://docs.rs/filecheck/latest/filecheck/).
62+
63+
**Note:** The output from the compiler is colorized, usually to red or yellow, and this involves
64+
printing ANSI escape sequences to the terminal. These sequences can confuse `FileCheck` as it tries
65+
to match patterns on 'word' boundaries. The first word in an error message is most likely prefixed
66+
with an escape sequence and can cause the check to fail.
67+
68+
To avoid this problem one may either not use the first word in the error message, or use the 'empty
69+
string' pattern `$()` to direct the matcher as to where the pattern starts.
70+
71+
E.g, `# check: $()The name "S" shadows another symbol with the same name.`
72+
5373
# Examples
5474

5575
The following is a common example for tests in the `should_pass/language` directory. The test
@@ -78,5 +98,18 @@ expected_result = { action = "result", value = 11 }
7898
contracts = ["should_pass/test_contracts/test_contract_a", "should_pass/test_contracts/test_contract_b"]
7999
```
80100

101+
Tests which fail can have fairly elaborate checks.
102+
103+
```toml
104+
category = "fail"
105+
106+
# check: // this asm block should return unit, i.e. nothing
107+
# nextln: asm(r1: 5) {
108+
# check: $()Mismatched types.
109+
# nextln: $()expected: ()
110+
# nextln: $()found: u64.
111+
# nextln: $()help: Implicit return must match up with block's type.
112+
```
113+
81114
And there are already hundreds of existing tests with `test.toml` descriptors which may be consulted
82115
when adding a new test.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
category = "fail"
2+
3+
# check: $()Storage attribute access mismatch. The trait function "test_function" in trait "MyContract" requires the storage attribute(s) #[storage(read)].
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
category = "fail"
2+
3+
# check: fn foo(s: str[7]) -> str[7] {
4+
# nextln: $()Expected: u64
5+
# nextln: $()found: str[7]. The definition of this function must match the one in the trait declaration.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
category = "fail"
2+
3+
# check: f()
4+
# nextln: $()Storage attribute access mismatch. Try giving the surrounding function more access by adding "#[storage(read)]" to the function declaration.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
category = "fail"
2+
3+
# check: return 42;
4+
# nextln: $()Mismatched types.
5+
# nextln: $()expected: ()
6+
# nextln: $()found: u64.
7+
# nextln: $()help: Return statement must return the declared function return type.
8+
9+
# This 'return true' line appears in both error messages..
10+
# check: return true;
11+
12+
# check: return true;
13+
# nextln: $()Mismatched types.
14+
# nextln: $()expected: ()
15+
# nextln: $()found: bool.
16+
# nextln: $()help: Return statement must return the declared function return type.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
category = "fail"
2+
3+
# check: ary[false]
4+
# nextln: $()Mismatched types.
5+
# nextln: $()expected: u64
6+
# nextln: $()found: bool.

0 commit comments

Comments
 (0)