Skip to content

Commit 94f0916

Browse files
martin-hughesIsaacWoods
authored andcommitted
Allow aml_tester to continue after a panic
Fix accidentally broken tests Exit with a failure code if needed This makes it easier for any user to see if a test has failed. Which may be useful for example in CI environments. Remove AML filename replacement `resolve_and_compile` relies on the current `iasl` way of naming its output, there's no need to duplicate that in `create_script_file`
1 parent 8c73cdc commit 94f0916

4 files changed

Lines changed: 243 additions & 112 deletions

File tree

tests/normal_fields.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ mod test_infra;
1616

1717
#[test]
1818
fn test_basic_store_and_load() {
19-
const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
19+
const AML: &str = r#"DefinitionBlock("", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
2020
OperationRegion(MEM, SystemMemory, 0x40000, 0x1000)
2121
Field(MEM, WordAcc, NoLock, Preserve) {
2222
A, 16,
@@ -46,7 +46,7 @@ fn test_basic_store_and_load() {
4646

4747
#[test]
4848
fn test_narrow_access_store_and_load() {
49-
const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
49+
const AML: &str = r#"DefinitionBlock("", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
5050
OperationRegion(MEM, SystemIO, 0x40, 0x10)
5151
Field(MEM, ByteAcc, NoLock, Preserve) {
5252
A, 16,
@@ -79,7 +79,7 @@ fn test_narrow_access_store_and_load() {
7979

8080
#[test]
8181
fn test_unaligned_field_store() {
82-
const AML: &str = r#"DefinitionBlock("%FN%", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
82+
const AML: &str = r#"DefinitionBlock("", "DSDT", 1, "RSACPI", "BUFFLD", 1) {
8383
OperationRegion(MEM, SystemIO, 0x40, 0x10)
8484
Field(MEM, WordAcc, NoLock, Preserve) {
8585
A, 7,

tests/test_infra/mod.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
use acpi::Handler;
2-
use aml_test_tools::{new_interpreter, run_test_for_string, TestResult};
3-
use aml_test_tools::handlers::logging_handler::LoggingHandler;
2+
use aml_test_tools::{
3+
RunTestResult,
4+
handlers::logging_handler::LoggingHandler,
5+
new_interpreter,
6+
run_test_for_string,
7+
};
48

59
pub fn run_aml_test(asl: &'static str, handler: impl Handler) {
610
// Tests calling `run_aml_test` don't do much else, and we usually want logging, so initialize it here.
711
let _ = pretty_env_logger::try_init();
8-
12+
913
let logged_handler = LoggingHandler::new(handler);
10-
let mut interpreter = new_interpreter(logged_handler);
14+
let interpreter = new_interpreter(logged_handler);
1115

12-
assert_eq!(run_test_for_string(asl, &mut interpreter), TestResult::Pass);
16+
assert!(matches!(run_test_for_string(asl, interpreter), RunTestResult::Pass(_)));
1317
}

tools/aml_test_tools/src/lib.rs

Lines changed: 147 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ use acpi::{
1414
use log::{error, trace};
1515
use std::{
1616
ffi::OsStr,
17+
fmt::Debug,
1718
fs::File,
1819
io::{Read, Write},
20+
panic::{AssertUnwindSafe, catch_unwind},
1921
path::PathBuf,
2022
process::Command,
2123
ptr::NonNull,
@@ -24,10 +26,59 @@ use std::{
2426
};
2527
use tempfile::{NamedTempFile, TempDir, tempdir};
2628

29+
/// The result of a call to [`run_test`]. This is a bit of a combination - it contains the actual
30+
/// result of the test, but also the interpreter - if it is still valid.
31+
pub enum RunTestResult<T>
32+
where
33+
T: Handler,
34+
{
35+
/// The test passed, and the interpreter is still valid.
36+
Pass(Interpreter<T>),
37+
38+
/// The test failed, but the interpreter is still valid. A failure reason is also provided.
39+
Failed(Interpreter<T>, TestFailureReason),
40+
41+
/// The test failed, and the interpreter is no longer valid.
42+
Panicked,
43+
}
44+
45+
/// The result of a test
46+
#[derive(Debug, PartialEq)]
47+
pub enum TestResult {
48+
/// The test passed.
49+
Pass,
50+
51+
/// The test failed.
52+
Failed(TestFailureReason),
53+
54+
/// The test caused a panic, probably in the interpreter. This is separated out from the failed
55+
/// case for two reasons:
56+
///
57+
/// 1. We want to highlight panics as they are in some ways worse than a failure - some AML
58+
/// might cause the problems for the interpreter, but the system as a whole shouldn't crash
59+
/// 2. The interpreter that was being used for testing is no longer valid - a new one is needed.
60+
Panicked,
61+
}
62+
63+
impl<T> From<&RunTestResult<T>> for TestResult
64+
where
65+
T: Handler,
66+
{
67+
fn from(result: &RunTestResult<T>) -> Self {
68+
match result {
69+
RunTestResult::Pass(_) => TestResult::Pass,
70+
RunTestResult::Failed(_, reason) => TestResult::Failed(reason.clone()),
71+
RunTestResult::Panicked => TestResult::Panicked,
72+
}
73+
}
74+
}
75+
2776
/// Possible results of [`resolve_and_compile`].
2877
pub enum CompilationOutcome {
2978
/// Not a valid ASL or AML file.
3079
Ignored,
80+
/// A filesystem error occurred.
81+
FilesystemErr(PathBuf),
3182
/// This file is already an AML file, does not need recompiling.
3283
IsAml(PathBuf),
3384
/// Both .asl and .aml files exist, and the .aml file is newer - it does not need recompiling.
@@ -41,17 +92,15 @@ pub enum CompilationOutcome {
4192
Succeeded(PathBuf),
4293
}
4394

44-
/// Possible results of [`run_test_for_file`].
45-
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
46-
pub enum TestResult {
47-
/// The test passed.
48-
Pass,
95+
/// Possible causes of a test failure.
96+
#[derive(Clone, Debug, /*Eq, Hash,*/ PartialEq)]
97+
pub enum TestFailureReason {
4998
/// The test ASL failed compilation by `iasl`.
5099
CompileFail,
51-
/// Our interpreter failed to parse the resulting AML.
52-
ParseFail,
53-
// TODO: should we do this??
54-
NotCompiled,
100+
/// Some error occurred attempting to read or write the test file.
101+
FilesystemErr,
102+
/// Our interpreter failed to parse or execute the resulting AML.
103+
ParseFail(AmlError),
55104
}
56105

57106
/// An internal-only struct that helps keep track of temporary files generated by
@@ -62,7 +111,6 @@ struct TempScriptFile {
62111
#[allow(unused)]
63112
dir: TempDir,
64113
asl_file: NamedTempFile,
65-
aml_file: PathBuf,
66114
}
67115

68116
/// Determine what to do with this file - ignore, compile and parse, or just parse.
@@ -73,47 +121,62 @@ struct TempScriptFile {
73121
/// - `path`: The filename of an ASL file to (possibly) compile.
74122
/// - `can_compile`: Whether compilation is allowed for this file. If not, any existing AML file
75123
/// with the same name (but different extension) will be used.
76-
pub fn resolve_and_compile(path: &PathBuf, can_compile: bool) -> std::io::Result<CompilationOutcome> {
124+
pub fn resolve_and_compile(path: &PathBuf, can_compile: bool) -> CompilationOutcome {
125+
let Ok(meta) = path.metadata() else {
126+
return CompilationOutcome::FilesystemErr(path.clone());
127+
};
128+
77129
// If this file is aml and it exists, it's ready for parsing
78130
// metadata() will error if the file does not exist
79-
if path.extension() == Some(OsStr::new("aml")) && path.metadata()?.is_file() {
80-
return Ok(CompilationOutcome::IsAml(path.clone()));
131+
if path.extension() == Some(OsStr::new("aml")) && meta.is_file() {
132+
return CompilationOutcome::IsAml(path.clone());
81133
}
82134

83135
// If this file is not asl, it's not interesting. Error if the file does not exist.
84-
if path.extension() != Some(OsStr::new("asl")) || !path.metadata()?.is_file() {
85-
return Ok(CompilationOutcome::Ignored);
136+
if path.extension() != Some(OsStr::new("asl")) || !meta.is_file() {
137+
return CompilationOutcome::Ignored;
86138
}
87139

88140
let aml_path = path.with_extension("aml");
89-
90141
if aml_path.is_file() {
91-
let asl_last_modified = path.metadata()?.modified()?;
92-
let aml_last_modified = aml_path.metadata()?.modified()?;
142+
let Ok(asl_last_modified) = meta.modified() else {
143+
return CompilationOutcome::FilesystemErr(path.clone());
144+
};
145+
let Ok(aml_last_modified) = aml_path.metadata().and_then(|m| m.modified()) else {
146+
return CompilationOutcome::FilesystemErr(path.clone());
147+
};
93148
// If the aml is more recent than the asl, use the existing aml
94149
// Otherwise continue to compilation
95150
if asl_last_modified <= aml_last_modified {
96-
return Ok(CompilationOutcome::Newer(aml_path));
151+
return CompilationOutcome::Newer(aml_path);
97152
}
98153
}
99154

100155
if !can_compile {
101-
return Ok(CompilationOutcome::NotCompiled(path.clone()));
156+
return CompilationOutcome::NotCompiled(path.clone());
102157
}
103158

104159
// Compile the ASL file using `iasl`
105160
println!("Compiling file: {}", path.display());
106-
let output = Command::new("iasl").arg(path).output()?;
161+
let output = Command::new("iasl").arg(path).output();
107162

108-
if !output.status.success() {
109-
println!(
110-
"Failed to compile ASL file: {}. Output from iasl:\n {}",
111-
path.display(),
112-
String::from_utf8_lossy(&output.stderr)
113-
);
114-
Ok(CompilationOutcome::Failed(path.clone()))
115-
} else {
116-
Ok(CompilationOutcome::Succeeded(aml_path))
163+
match output {
164+
Ok(output) => {
165+
if !output.status.success() {
166+
println!(
167+
"Failed to compile ASL file: {}. Output from iasl:\n {}",
168+
path.display(),
169+
String::from_utf8_lossy(&output.stderr)
170+
);
171+
CompilationOutcome::Failed(path.clone())
172+
} else {
173+
CompilationOutcome::Succeeded(aml_path)
174+
}
175+
}
176+
Err(e) => {
177+
println!("Failed to compile ASL file: {}. Error: {}", path.display(), e);
178+
CompilationOutcome::Failed(path.clone())
179+
}
117180
}
118181
}
119182

@@ -185,11 +248,17 @@ where
185248
/// * `asl`: A string slice containing an ASL script. This will be compiled to AML using `iasl` and
186249
/// then tested using [`run_test`]
187250
/// * `interpreter`: The interpreter to use for testing.
188-
pub fn run_test_for_string(asl: &'static str, interpreter: &mut Interpreter<impl Handler + Clone>) -> TestResult {
251+
pub fn run_test_for_string<T>(asl: &'static str, interpreter: Interpreter<T>) -> RunTestResult<T>
252+
where
253+
T: Handler,
254+
{
189255
let script = create_script_file(asl);
190-
resolve_and_compile(&script.asl_file.path().to_path_buf(), true).unwrap();
191-
192-
run_test_for_file(&script.aml_file, interpreter)
256+
match resolve_and_compile(&script.asl_file.path().to_path_buf(), true) {
257+
CompilationOutcome::Succeeded(aml_path) | CompilationOutcome::IsAml(aml_path) => {
258+
run_test_for_file(&aml_path, interpreter)
259+
}
260+
_ => RunTestResult::Failed(interpreter, TestFailureReason::CompileFail),
261+
}
193262
}
194263

195264
/// Test an AML file using [`run_test`]
@@ -199,23 +268,20 @@ pub fn run_test_for_string(asl: &'static str, interpreter: &mut Interpreter<impl
199268
/// * `file`: The path to the AML file to test. This must be an AML file otherwise the test will
200269
/// fail very quickly.
201270
/// * `interpreter`: The interpreter to use for testing.
202-
pub fn run_test_for_file(file: &PathBuf, interpreter: &mut Interpreter<impl Handler + Clone>) -> TestResult {
203-
let Ok(mut file) = File::open(&file) else {
204-
return TestResult::CompileFail;
271+
pub fn run_test_for_file<T>(file: &PathBuf, interpreter: Interpreter<T>) -> RunTestResult<T>
272+
where
273+
T: Handler,
274+
{
275+
let Ok(mut file) = File::open(file) else {
276+
return RunTestResult::Failed(interpreter, TestFailureReason::FilesystemErr);
205277
};
206278
let mut contents = Vec::new();
207279
file.read_to_end(&mut contents).unwrap();
208280

209281
const AML_TABLE_HEADER_LENGTH: usize = 36;
210282
let stream = &contents[AML_TABLE_HEADER_LENGTH..];
211283

212-
match run_test(stream, interpreter) {
213-
Ok(()) => TestResult::Pass,
214-
Err(e) => {
215-
error!("Error running test: {:?}", e);
216-
TestResult::ParseFail
217-
}
218-
}
284+
run_test(stream, interpreter)
219285
}
220286

221287
/// Internal function to create a temporary script file from an ASL string, plus to calculate the
@@ -226,16 +292,10 @@ fn create_script_file(asl: &'static str) -> TempScriptFile {
226292
let mut script_file = NamedTempFile::with_suffix_in(".asl", &dir).unwrap();
227293
trace!("Created temp file: {:?}", script_file.path());
228294

229-
let output_stem = script_file.path().file_stem().unwrap().to_str().unwrap();
230-
let output_name = format!("{}.aml", output_stem);
231-
let output_full_name = dir.path().join(output_name.clone());
232-
233-
let new_asl = asl.replace("%FN%", output_name.as_str());
234-
235-
script_file.write_all(new_asl.as_bytes()).unwrap();
295+
script_file.write_all(asl.as_bytes()).unwrap();
236296
script_file.flush().unwrap();
237297

238-
TempScriptFile { dir, asl_file: script_file, aml_file: output_full_name }
298+
TempScriptFile { dir, asl_file: script_file }
239299
}
240300

241301
/// Run a test on an AML stream.
@@ -248,25 +308,43 @@ fn create_script_file(asl: &'static str) -> TempScriptFile {
248308
/// Arguments:
249309
///
250310
/// * `stream`: A slice containing the AML bytecode to test.
251-
/// * `interpreter`: The interpreter to test with.
252-
pub fn run_test(stream: &[u8], interpreter: &mut Interpreter<impl Handler + Clone>) -> Result<(), AmlError> {
253-
interpreter.load_table(stream)?;
311+
/// * `interpreter`: The interpreter to test with. The interpreter is consumed to maintain unwind
312+
/// safety - if the interpreter panics, the caller should not be able to see the interpreter in
313+
/// an inconsistent state.
314+
pub fn run_test<T>(stream: &[u8], interpreter: Interpreter<T>) -> RunTestResult<T>
315+
where
316+
T: Handler,
317+
{
318+
// Without `AssertUnwindSafe`, the following code will not build as the Interpreter is not
319+
// unwind safe. To avoid the caller being able to see an inconsistent Interpreter, if a panic
320+
// occurs we drop the Interpreter, forcing the caller to create a new one.
321+
let result = catch_unwind(AssertUnwindSafe(|| -> Result<(), AmlError> {
322+
interpreter.load_table(stream)?;
254323

255-
if let Some(result) = interpreter.evaluate_if_present(AmlName::from_str("\\MAIN").unwrap(), vec![])? {
256-
match *result {
257-
Object::Integer(0) => Ok(()),
258-
Object::Integer(other) => {
259-
error!("Test _MAIN returned non-zero exit code: {}", other);
260-
// TODO: wrong error - this should probs return a more complex err type
261-
Err(AmlError::NoCurrentOp)
262-
}
263-
_ => {
264-
error!("Test _MAIN returned unexpected object type: {}", *result);
265-
// TODO: wrong error
266-
Err(AmlError::NoCurrentOp)
324+
if let Some(result) = interpreter.evaluate_if_present(AmlName::from_str("\\MAIN").unwrap(), vec![])? {
325+
match *result {
326+
Object::Integer(0) => Ok(()),
327+
Object::Integer(other) => {
328+
error!("Test _MAIN returned non-zero exit code: {}", other);
329+
// TODO: wrong error - this should probs return a more complex err type
330+
Err(AmlError::NoCurrentOp)
331+
}
332+
_ => {
333+
error!("Test _MAIN returned unexpected object type: {}", *result);
334+
// TODO: wrong error
335+
Err(AmlError::NoCurrentOp)
336+
}
267337
}
338+
} else {
339+
Ok(())
268340
}
269-
} else {
270-
Ok(())
341+
}));
342+
343+
match result {
344+
Ok(inner) => match inner {
345+
Ok(()) => RunTestResult::Pass(interpreter),
346+
Err(e) => RunTestResult::Failed(interpreter, TestFailureReason::ParseFail(e)),
347+
},
348+
Err(_e) => RunTestResult::Panicked,
271349
}
272350
}

0 commit comments

Comments
 (0)