Skip to content

Commit dca6e12

Browse files
test: save test results
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 62e065e commit dca6e12

File tree

4 files changed

+350
-0
lines changed

4 files changed

+350
-0
lines changed

crates/tinywasm/tests/mvp.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.0.4,9258,7567,[{"name":"0","passed":0,"failed":54},{"name":"0","passed":0,"failed":109},{"name":"66","passed":66,"failed":25},{"name":"104","passed":104,"failed":8},{"name":"0","passed":0,"failed":171},{"name":"0","passed":0,"failed":21},{"name":"0","passed":0,"failed":30},{"name":"0","passed":0,"failed":25},{"name":"0","passed":0,"failed":22},{"name":"0","passed":0,"failed":56},{"name":"4","passed":4,"failed":4},{"name":"702","passed":702,"failed":76},{"name":"0","passed":0,"failed":93},{"name":"10","passed":10,"failed":1},{"name":"0","passed":0,"failed":61},{"name":"0","passed":0,"failed":76},{"name":"0","passed":0,"failed":1},{"name":"21","passed":21,"failed":73},{"name":"1005","passed":1005,"failed":1509},{"name":"1","passed":1,"failed":363},{"name":"2401","passed":2401,"failed":6},{"name":"1005","passed":1005,"failed":1509},{"name":"1","passed":1,"failed":363},{"name":"2401","passed":2401,"failed":6},{"name":"0","passed":0,"failed":2},{"name":"269","passed":269,"failed":591},{"name":"34","passed":34,"failed":129},{"name":"0","passed":0,"failed":6},{"name":"138","passed":138,"failed":303},{"name":"1","passed":1,"failed":4},{"name":"4","passed":4,"failed":75},{"name":"0","passed":0,"failed":16},{"name":"4","passed":4,"failed":49},{"name":"0","passed":0,"failed":96},{"name":"0","passed":0,"failed":42},{"name":"0","passed":0,"failed":118},{"name":"1","passed":1,"failed":156},{"name":"0","passed":0,"failed":1},{"name":"38","passed":38,"failed":70},{"name":"5","passed":5,"failed":46},{"name":"1","passed":1,"failed":28},{"name":"0","passed":0,"failed":1},{"name":"1","passed":1,"failed":66},{"name":"0","passed":0,"failed":60},{"name":"2","passed":2,"failed":34},{"name":"5","passed":5,"failed":48},{"name":"0","passed":0,"failed":42},{"name":"0","passed":0,"failed":43},{"name":"0","passed":0,"failed":34},{"name":"0","passed":0,"failed":19},{"name":"0","passed":0,"failed":1},{"name":"0","passed":0,"failed":6},{"name":"0","passed":0,"failed":172},{"name":"484","passed":484,"failed":1},{"name":"0","passed":0,"failed":5},{"name":"0","passed":0,"failed":21},{"name":"0","passed":0,"failed":32},{"name":"0","passed":0,"failed":11},{"name":"0","passed":0,"failed":2},{"name":"0","passed":0,"failed":10},{"name":"0","passed":0,"failed":59},{"name":"1","passed":1,"failed":27},{"name":"16","passed":16,"failed":42},{"name":"3","passed":3,"failed":33},{"name":"1","passed":1,"failed":2},{"name":"0","passed":0,"failed":59},{"name":"0","passed":0,"failed":118},{"name":"1","passed":1,"failed":49},{"name":"176","passed":176,"failed":0},{"name":"176","passed":176,"failed":0},{"name":"176","passed":176,"failed":0},{"name":"0","passed":0,"failed":176}]
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
use eyre::Result;
2+
use std::io::{BufRead, Seek, SeekFrom};
3+
use std::{
4+
collections::BTreeMap,
5+
fmt::{Debug, Formatter},
6+
io::BufReader,
7+
};
8+
9+
mod run;
10+
mod util;
11+
12+
use serde::{Deserialize, Serialize};
13+
14+
#[derive(Serialize, Deserialize)]
15+
struct TestGroupResult {
16+
name: String,
17+
passed: usize,
18+
failed: usize,
19+
}
20+
21+
pub struct TestSuite(BTreeMap<String, TestGroup>);
22+
23+
impl TestSuite {
24+
pub fn new() -> Self {
25+
Self(BTreeMap::new())
26+
}
27+
28+
pub fn failed(&self) -> bool {
29+
self.0.values().any(|group| group.stats().1 > 0)
30+
}
31+
32+
fn test_group(&mut self, name: &str) -> &mut TestGroup {
33+
self.0.entry(name.to_string()).or_insert_with(TestGroup::new)
34+
}
35+
36+
// create or add to a test result file
37+
pub fn save_csv(&self, path: &str, version: &str) -> Result<()> {
38+
use std::fs::OpenOptions;
39+
use std::io::Write;
40+
41+
let mut file = OpenOptions::new().create(true).append(true).read(true).open(path)?;
42+
let last_line = BufReader::new(&file).lines().last().transpose()?;
43+
44+
// Check if the last line starts with the current commit
45+
if let Some(last) = last_line {
46+
if last.starts_with(version) {
47+
// Truncate the file size to remove the last line
48+
let len_to_truncate = last.len() as i64;
49+
file.set_len(file.metadata()?.len() - len_to_truncate as u64)?;
50+
}
51+
}
52+
53+
// Seek to the end of the file for appending
54+
file.seek(SeekFrom::End(0))?;
55+
56+
let mut passed = 0;
57+
let mut failed = 0;
58+
59+
let mut groups = Vec::new();
60+
for group in self.0.values() {
61+
let (group_passed, group_failed) = group.stats();
62+
passed += group_passed;
63+
failed += group_failed;
64+
65+
groups.push(TestGroupResult {
66+
name: group_passed.to_string(),
67+
passed: group_passed,
68+
failed: group_failed,
69+
});
70+
}
71+
72+
let groups = serde_json::to_string(&groups)?;
73+
let line = format!("{},{},{},{}\n", version, passed, failed, groups);
74+
file.write_all(line.as_bytes()).expect("failed to write to csv file");
75+
76+
Ok(())
77+
}
78+
}
79+
80+
impl Debug for TestSuite {
81+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82+
use owo_colors::OwoColorize;
83+
let mut total_passed = 0;
84+
let mut total_failed = 0;
85+
86+
for (group_name, group) in &self.0 {
87+
let (group_passed, group_failed) = group.stats();
88+
total_passed += group_passed;
89+
total_failed += group_failed;
90+
91+
writeln!(f, "{}", group_name.bold().underline())?;
92+
writeln!(f, " Tests Passed: {}", group_passed.to_string().green())?;
93+
writeln!(f, " Tests Failed: {}", group_failed.to_string().red())?;
94+
95+
// for (test_name, test) in &group.tests {
96+
// write!(f, " {}: ", test_name.bold())?;
97+
// match &test.result {
98+
// Ok(()) => {
99+
// writeln!(f, "{}", "Passed".green())?;
100+
// }
101+
// Err(e) => {
102+
// writeln!(f, "{}", "Failed".red())?;
103+
// // writeln!(f, "Error: {:?}", e)?;
104+
// }
105+
// }
106+
// writeln!(f, " Span: {:?}", test.span)?;
107+
// }
108+
}
109+
110+
writeln!(f, "\n{}", "Total Test Summary:".bold().underline())?;
111+
writeln!(f, " Total Tests: {}", (total_passed + total_failed))?;
112+
writeln!(f, " Total Passed: {}", total_passed.to_string().green())?;
113+
writeln!(f, " Total Failed: {}", total_failed.to_string().red())?;
114+
Ok(())
115+
}
116+
}
117+
118+
struct TestGroup {
119+
tests: BTreeMap<String, TestCase>,
120+
}
121+
122+
impl TestGroup {
123+
fn new() -> Self {
124+
Self { tests: BTreeMap::new() }
125+
}
126+
127+
fn stats(&self) -> (usize, usize) {
128+
let mut passed_count = 0;
129+
let mut failed_count = 0;
130+
131+
for test in self.tests.values() {
132+
match test.result {
133+
Ok(()) => passed_count += 1,
134+
Err(_) => failed_count += 1,
135+
}
136+
}
137+
138+
(passed_count, failed_count)
139+
}
140+
141+
fn add_result(&mut self, name: &str, span: wast::token::Span, result: Result<()>) {
142+
self.tests.insert(name.to_string(), TestCase { result, _span: span });
143+
}
144+
}
145+
146+
struct TestCase {
147+
result: Result<()>,
148+
_span: wast::token::Span,
149+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use crate::testsuite::util::{parse_module, wastarg2tinywasmvalue, wastret2tinywasmvalue};
2+
3+
use super::TestSuite;
4+
use eyre::{eyre, Result};
5+
use log::debug;
6+
use tinywasm_types::TinyWasmModule;
7+
use wast::{lexer::Lexer, parser::ParseBuffer, QuoteWat, Wast};
8+
9+
impl TestSuite {
10+
pub fn run(&mut self, tests: &[&str]) -> Result<()> {
11+
tests.iter().for_each(|group| {
12+
let test_group = self.test_group(group);
13+
14+
let wast = wasm_testsuite::get_test_wast(group).expect("failed to get test wast");
15+
let wast = std::str::from_utf8(&wast).expect("failed to convert wast to utf8");
16+
17+
let mut lexer = Lexer::new(wast);
18+
// we need to allow confusing unicode characters since they are technically valid wasm
19+
lexer.allow_confusing_unicode(true);
20+
21+
let buf = ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer");
22+
let wast_data = wast::parser::parse::<Wast>(&buf).expect("failed to parse wat");
23+
24+
let mut last_module: Option<TinyWasmModule> = None;
25+
for (i, directive) in wast_data.directives.into_iter().enumerate() {
26+
let span = directive.span();
27+
use wast::WastDirective::*;
28+
let name = format!("{}-{}", group, i);
29+
30+
match directive {
31+
// TODO: needs to support more binary sections
32+
Wat(QuoteWat::Wat(wast::Wat::Module(module))) => {
33+
let result = std::panic::catch_unwind(|| parse_module(module))
34+
.map_err(|e| eyre!("failed to parse module: {:?}", e))
35+
.and_then(|res| res);
36+
37+
match &result {
38+
Err(_) => last_module = None,
39+
Ok(m) => last_module = Some(m.clone()),
40+
}
41+
42+
test_group.add_result(&format!("{}-parse", name), span, result.map(|_| ()));
43+
}
44+
45+
// these all pass already :)
46+
AssertMalformed {
47+
span,
48+
module: QuoteWat::Wat(wast::Wat::Module(module)),
49+
message: _,
50+
} => {
51+
let res = std::panic::catch_unwind(|| parse_module(module).map(|_| ()));
52+
test_group.add_result(
53+
&format!("{}-malformed", name),
54+
span,
55+
match res {
56+
Ok(Ok(_)) => Err(eyre!("expected module to be malformed")),
57+
Err(_) | Ok(Err(_)) => Ok(()),
58+
},
59+
);
60+
}
61+
62+
AssertReturn { span, exec, results } => {
63+
let Some(module) = last_module.as_ref() else {
64+
// we skip tests for modules that failed to parse
65+
println!("no module found for assert_return: {:?}", exec);
66+
continue;
67+
};
68+
69+
let res: Result<Result<()>, _> = std::panic::catch_unwind(|| {
70+
let mut store = tinywasm::Store::new();
71+
let module = tinywasm::Module::from(module);
72+
let instance = module.instantiate(&mut store)?;
73+
74+
use wast::WastExecute::*;
75+
let invoke = match exec {
76+
Wat(_) => return Result::Ok(()), // not used by the testsuite
77+
Get { module: _, global: _ } => return Result::Ok(()),
78+
Invoke(invoke) => invoke,
79+
};
80+
81+
let args = invoke
82+
.args
83+
.into_iter()
84+
.map(wastarg2tinywasmvalue)
85+
.collect::<Result<Vec<_>>>()?;
86+
let res = instance.get_func(&store, invoke.name)?.call(&mut store, &args)?;
87+
let expected = results
88+
.into_iter()
89+
.map(wastret2tinywasmvalue)
90+
.collect::<Result<Vec<_>>>()?;
91+
92+
if res.len() != expected.len() {
93+
return Result::Err(eyre!("expected {} results, got {}", expected.len(), res.len()));
94+
}
95+
96+
for (i, (res, expected)) in res.iter().zip(expected).enumerate() {
97+
if res != &expected {
98+
return Result::Err(eyre!(
99+
"result {} did not match: {:?} != {:?}",
100+
i,
101+
res,
102+
expected
103+
));
104+
}
105+
}
106+
107+
Ok(())
108+
});
109+
110+
let res = match res {
111+
Err(e) => Err(eyre!("test panicked: {:?}", e)),
112+
Ok(Err(e)) => Err(e),
113+
Ok(Ok(())) => Ok(()),
114+
};
115+
116+
test_group.add_result(&format!("{}-return", name), span, res);
117+
}
118+
Invoke(m) => {
119+
debug!("invoke: {:?}", m);
120+
}
121+
_ => test_group.add_result(&format!("{}-unknown", name), span, Err(eyre!("unsupported directive"))),
122+
}
123+
}
124+
});
125+
126+
Ok(())
127+
}
128+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use eyre::{eyre, Result};
2+
use tinywasm_types::TinyWasmModule;
3+
4+
pub fn parse_module(mut module: wast::core::Module) -> Result<TinyWasmModule> {
5+
let parser = tinywasm_parser::Parser::new();
6+
Ok(parser.parse_module_bytes(module.encode().expect("failed to encode module"))?)
7+
}
8+
9+
pub fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result<tinywasm_types::WasmValue> {
10+
let wast::WastArg::Core(arg) = arg else {
11+
return Err(eyre!("unsupported arg type"));
12+
};
13+
14+
use tinywasm_types::WasmValue;
15+
use wast::core::WastArgCore::*;
16+
Ok(match arg {
17+
F32(f) => WasmValue::F32(f32::from_bits(f.bits)),
18+
F64(f) => WasmValue::F64(f64::from_bits(f.bits)),
19+
I32(i) => WasmValue::I32(i),
20+
I64(i) => WasmValue::I64(i),
21+
_ => return Err(eyre!("unsupported arg type")),
22+
})
23+
}
24+
25+
pub fn wastret2tinywasmvalue(arg: wast::WastRet) -> Result<tinywasm_types::WasmValue> {
26+
let wast::WastRet::Core(arg) = arg else {
27+
return Err(eyre!("unsupported arg type"));
28+
};
29+
30+
use tinywasm_types::WasmValue;
31+
use wast::core::WastRetCore::*;
32+
Ok(match arg {
33+
F32(f) => nanpattern2tinywasmvalue(f)?,
34+
F64(f) => nanpattern2tinywasmvalue(f)?,
35+
I32(i) => WasmValue::I32(i),
36+
I64(i) => WasmValue::I64(i),
37+
_ => return Err(eyre!("unsupported arg type")),
38+
})
39+
}
40+
41+
enum Bits {
42+
U32(u32),
43+
U64(u64),
44+
}
45+
trait FloatToken {
46+
fn bits(&self) -> Bits;
47+
}
48+
impl FloatToken for wast::token::Float32 {
49+
fn bits(&self) -> Bits {
50+
Bits::U32(self.bits)
51+
}
52+
}
53+
impl FloatToken for wast::token::Float64 {
54+
fn bits(&self) -> Bits {
55+
Bits::U64(self.bits)
56+
}
57+
}
58+
59+
fn nanpattern2tinywasmvalue<T>(arg: wast::core::NanPattern<T>) -> Result<tinywasm_types::WasmValue>
60+
where
61+
T: FloatToken,
62+
{
63+
use wast::core::NanPattern::*;
64+
Ok(match arg {
65+
CanonicalNan => tinywasm_types::WasmValue::F32(f32::NAN),
66+
ArithmeticNan => tinywasm_types::WasmValue::F32(f32::NAN),
67+
Value(v) => match v.bits() {
68+
Bits::U32(v) => tinywasm_types::WasmValue::F32(f32::from_bits(v)),
69+
Bits::U64(v) => tinywasm_types::WasmValue::F64(f64::from_bits(v)),
70+
},
71+
})
72+
}

0 commit comments

Comments
 (0)