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