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
main() -> Result<()>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
find_tests(path: &Path, dst: &mut Vec<PathBuf>) -> Result<()>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
run_test(path: &Path) -> Result<()>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 {
to_vec(&self) -> Vec<&str>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.
new(path: &Path) -> Result<Test>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.
compile(&mut self) -> Result<CompileOutput>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`.
assert_output(test: &Test, output: CompileOutput) -> Result<()>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
assert_or_bless_output(path: &Path, wat: &str, actual: &str) -> Result<()>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