Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/tests/disas.rs
1685 views
1
//! A filetest-lookalike test suite using Cranelift tooling but built on
2
//! Wasmtime's code generator.
3
//!
4
//! This test will read the `tests/disas/*` directory and interpret all files in
5
//! that directory as a test. Each test must be in the wasm text format and
6
//! start with directives that look like:
7
//!
8
//! ```wasm
9
//! ;;! target = "x86_64"
10
//! ;;! compile = true
11
//!
12
//! (module
13
//! ;; ...
14
//! )
15
//! ```
16
//!
17
//! Tests must configure a `target` and then can optionally specify a kind of
18
//! test:
19
//!
20
//! * No specifier - the output CLIF from translation is inspected.
21
//! * `optimize = true` - CLIF is emitted, then optimized, then inspected.
22
//! * `compile = true` - backends are run to produce machine code and that's inspected.
23
//!
24
//! Tests may also have a `flags` directive which are CLI flags to Wasmtime
25
//! itself:
26
//!
27
//! ```wasm
28
//! ;;! target = "x86_64"
29
//! ;;! flags = "-O opt-level=s"
30
//!
31
//! (module
32
//! ;; ...
33
//! )
34
//! ```
35
//!
36
//! Flags are parsed by the `wasmtime_cli_flags` crate to build a `Config`.
37
//!
38
//! Configuration of tests is prefixed with `;;!` comments and must be present
39
//! at the start of the file. These comments are then parsed as TOML and
40
//! deserialized into `TestConfig` in this crate.
41
42
use anyhow::{Context, Result, bail};
43
use clap::Parser;
44
use cranelift_codegen::ir::Function;
45
use libtest_mimic::{Arguments, Trial};
46
use serde_derive::Deserialize;
47
use similar::TextDiff;
48
use std::fmt::Write as _;
49
use std::io::Write as _;
50
use std::path::{Path, PathBuf};
51
use std::process::Stdio;
52
use tempfile::TempDir;
53
use wasmtime::{Engine, OptLevel, Strategy};
54
use wasmtime_cli_flags::CommonOptions;
55
56
fn main() -> Result<()> {
57
if cfg!(miri) || cfg!(asan) {
58
return Ok(());
59
}
60
61
// There's not a ton of use in emulating these tests on other architectures
62
// since they only exercise architecture-independent code of compiling to
63
// multiple architectures. Additionally CI seems to occasionally deadlock or
64
// get stuck in these tests when using QEMU, and it's not entirely clear
65
// why. Finally QEMU-emulating these tests is relatively slow and without
66
// much benefit from emulation it's hard to justify this. In the end disable
67
// this test suite when QEMU is enabled.
68
if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() {
69
return Ok(());
70
}
71
72
let _ = env_logger::try_init();
73
74
let mut tests = Vec::new();
75
find_tests("./tests/disas".as_ref(), &mut tests)?;
76
77
let mut trials = Vec::new();
78
for test in tests {
79
trials.push(Trial::test(test.to_str().unwrap().to_string(), move || {
80
run_test(&test)
81
.with_context(|| format!("failed to run tests {test:?}"))
82
.map_err(|e| format!("{e:?}").into())
83
}))
84
}
85
86
// These tests have some long names so use the "quiet" output by default.
87
let mut arguments = Arguments::parse();
88
if arguments.format.is_none() {
89
arguments.quiet = true;
90
}
91
libtest_mimic::run(&arguments, trials).exit()
92
}
93
94
fn find_tests(path: &Path, dst: &mut Vec<PathBuf>) -> Result<()> {
95
for file in path
96
.read_dir()
97
.with_context(|| format!("failed to read {path:?}"))?
98
{
99
let file = file.context("failed to read directory entry")?;
100
let path = file.path();
101
if file.file_type()?.is_dir() {
102
find_tests(&path, dst)?;
103
} else if path.extension().and_then(|s| s.to_str()) == Some("wat") {
104
dst.push(path);
105
}
106
}
107
Ok(())
108
}
109
110
fn run_test(path: &Path) -> Result<()> {
111
let mut test = Test::new(path)?;
112
let output = test.compile()?;
113
114
assert_output(&test, output)?;
115
116
Ok(())
117
}
118
#[derive(Debug, Deserialize)]
119
#[serde(deny_unknown_fields)]
120
struct TestConfig {
121
target: String,
122
#[serde(default)]
123
test: TestKind,
124
flags: Option<TestConfigFlags>,
125
objdump: Option<TestConfigFlags>,
126
filter: Option<String>,
127
}
128
129
#[derive(Debug, Deserialize)]
130
#[serde(untagged)]
131
enum TestConfigFlags {
132
SpaceSeparated(String),
133
List(Vec<String>),
134
}
135
136
impl TestConfigFlags {
137
fn to_vec(&self) -> Vec<&str> {
138
match self {
139
TestConfigFlags::SpaceSeparated(s) => s.split_whitespace().collect(),
140
TestConfigFlags::List(s) => s.iter().map(|s| s.as_str()).collect(),
141
}
142
}
143
}
144
145
struct Test {
146
path: PathBuf,
147
contents: String,
148
opts: CommonOptions,
149
config: TestConfig,
150
}
151
152
/// Which kind of test is being performed.
153
#[derive(Default, Debug, Deserialize)]
154
#[serde(rename_all = "lowercase")]
155
enum TestKind {
156
/// Test the CLIF output, raw from translation.
157
#[default]
158
Clif,
159
/// Compile output to machine code.
160
Compile,
161
/// Test the CLIF output, optimized.
162
Optimize,
163
/// Alias for "compile" plus `-C compiler=winch`
164
Winch,
165
}
166
167
impl Test {
168
/// Parse the contents of `path` looking for directive-based comments
169
/// starting with `;;!` near the top of the file.
170
fn new(path: &Path) -> Result<Test> {
171
let contents =
172
std::fs::read_to_string(path).with_context(|| format!("failed to read {path:?}"))?;
173
let config: TestConfig = wasmtime_test_util::wast::parse_test_config(&contents, ";;!")
174
.context("failed to parse test configuration as TOML")?;
175
let mut flags = vec!["wasmtime"];
176
if let Some(config) = &config.flags {
177
flags.extend(config.to_vec());
178
}
179
let mut opts = wasmtime_cli_flags::CommonOptions::try_parse_from(&flags)?;
180
opts.codegen.cranelift_debug_verifier = Some(true);
181
182
Ok(Test {
183
path: path.to_path_buf(),
184
config,
185
opts,
186
contents,
187
})
188
}
189
190
/// Generates CLIF for all the wasm functions in this test.
191
fn compile(&mut self) -> Result<CompileOutput> {
192
// Use wasmtime::Config with its `emit_clif` option to get Wasmtime's
193
// code generator to jettison CLIF out the back.
194
let tempdir = TempDir::new().context("failed to make a tempdir")?;
195
let mut config = self.opts.config(None)?;
196
config.target(&self.config.target)?;
197
match self.config.test {
198
TestKind::Clif => {
199
config.emit_clif(tempdir.path());
200
config.cranelift_opt_level(OptLevel::None);
201
}
202
TestKind::Optimize => {
203
config.emit_clif(tempdir.path());
204
}
205
TestKind::Compile => {}
206
TestKind::Winch => {
207
config.strategy(Strategy::Winch);
208
}
209
}
210
let engine = Engine::new(&config).context("failed to create engine")?;
211
let wasm = wat::parse_file(&self.path)?;
212
let elf = if wasmparser::Parser::is_component(&wasm) {
213
engine
214
.precompile_component(&wasm)
215
.context("failed to compile component")?
216
} else {
217
engine
218
.precompile_module(&wasm)
219
.context("failed to compile module")?
220
};
221
222
match self.config.test {
223
TestKind::Clif | TestKind::Optimize => {
224
// Read all `*.clif` files from the clif directory that the
225
// compilation process just emitted.
226
let mut clifs = Vec::new();
227
228
// Sort entries for determinism; multiple wasm modules can
229
// generate clif functions with the same names, so sorting the
230
// resulting clif functions alone isn't good enough.
231
let mut entries = tempdir
232
.path()
233
.read_dir()
234
.context("failed to read tempdir")?
235
.map(|e| Ok(e.context("failed to iterate over tempdir")?.path()))
236
.collect::<Result<Vec<_>>>()?;
237
entries.sort();
238
239
for path in entries {
240
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
241
let filter = self.config.filter.as_deref().unwrap_or("wasm[0]--function");
242
if !name.contains(filter) {
243
continue;
244
}
245
}
246
let clif = std::fs::read_to_string(&path)
247
.with_context(|| format!("failed to read clif file {path:?}"))?;
248
clifs.push(clif);
249
}
250
251
// Parse the text format CLIF which is emitted by Wasmtime back
252
// into in-memory data structures.
253
let functions = clifs
254
.iter()
255
.map(|clif| {
256
let mut funcs = cranelift_reader::parse_functions(clif)?;
257
if funcs.len() != 1 {
258
bail!("expected one function per clif");
259
}
260
Ok(funcs.remove(0))
261
})
262
.collect::<Result<Vec<_>>>()?;
263
264
Ok(CompileOutput::Clif(functions))
265
}
266
TestKind::Compile | TestKind::Winch => Ok(CompileOutput::Elf(elf)),
267
}
268
}
269
}
270
271
enum CompileOutput {
272
Clif(Vec<Function>),
273
Elf(Vec<u8>),
274
}
275
276
/// Assert that `wat` contains the test expectations necessary for `funcs`.
277
fn assert_output(test: &Test, output: CompileOutput) -> Result<()> {
278
let mut actual = String::new();
279
match output {
280
CompileOutput::Clif(funcs) => {
281
for mut func in funcs {
282
func.dfg.resolve_all_aliases();
283
writeln!(&mut actual, "{}", func.display()).unwrap();
284
}
285
}
286
CompileOutput::Elf(bytes) => {
287
let mut cmd = wasmtime_test_util::command(env!("CARGO_BIN_EXE_wasmtime"));
288
cmd.arg("objdump")
289
.arg("--address-width=4")
290
.arg("--address-jumps")
291
.stdin(Stdio::piped())
292
.stdout(Stdio::piped())
293
.stderr(Stdio::piped());
294
match &test.config.objdump {
295
Some(args) => {
296
cmd.args(args.to_vec());
297
}
298
None => {
299
cmd.arg("--traps=false");
300
}
301
}
302
if let Some(filter) = &test.config.filter {
303
cmd.arg("--filter").arg(filter);
304
}
305
306
let mut child = cmd.spawn().context("failed to run wasmtime")?;
307
child
308
.stdin
309
.take()
310
.unwrap()
311
.write_all(&bytes)
312
.context("failed to write stdin")?;
313
let output = child
314
.wait_with_output()
315
.context("failed to wait for child")?;
316
if !output.status.success() {
317
bail!(
318
"objdump failed: {}\nstderr: {}",
319
output.status,
320
String::from_utf8_lossy(&output.stderr),
321
);
322
}
323
actual = String::from_utf8(output.stdout).unwrap();
324
}
325
}
326
let actual = actual.trim();
327
assert_or_bless_output(&test.path, &test.contents, actual)
328
}
329
330
fn assert_or_bless_output(path: &Path, wat: &str, actual: &str) -> Result<()> {
331
log::debug!("=== actual ===\n{actual}");
332
// The test's expectation is the final comment.
333
let mut expected_lines: Vec<_> = wat
334
.lines()
335
.rev()
336
.map_while(|l| l.strip_prefix(";;"))
337
.map(|l| l.strip_prefix(" ").unwrap_or(l))
338
.collect();
339
expected_lines.reverse();
340
let expected = expected_lines.join("\n");
341
let expected = expected.trim();
342
log::debug!("=== expected ===\n{expected}");
343
344
if actual == expected {
345
return Ok(());
346
}
347
348
if std::env::var("WASMTIME_TEST_BLESS").unwrap_or_default() == "1" {
349
let old_expectation_line_count = wat
350
.lines()
351
.rev()
352
.take_while(|l| l.starts_with(";;"))
353
.count();
354
let old_wat_line_count = wat.lines().count();
355
let new_wat_lines: Vec<_> = wat
356
.lines()
357
.take(old_wat_line_count - old_expectation_line_count)
358
.map(|l| l.to_string())
359
.chain(actual.lines().map(|l| {
360
if l.is_empty() {
361
";;".to_string()
362
} else {
363
format!(";; {l}")
364
}
365
}))
366
.collect();
367
let mut new_wat = new_wat_lines.join("\n");
368
new_wat.push('\n');
369
std::fs::write(path, new_wat)
370
.with_context(|| format!("failed to write file: {}", path.display()))?;
371
return Ok(());
372
}
373
374
bail!(
375
"Did not get the expected CLIF translation:\n\n\
376
{}\n\n\
377
Note: You can re-run with the `WASMTIME_TEST_BLESS=1` environment\n\
378
variable set to update test expectations.",
379
TextDiff::from_lines(expected, actual)
380
.unified_diff()
381
.header("expected", "actual")
382
)
383
}
384
385