xref: /wasmtime-44.0.1/tests/disas.rs (revision 94740588)
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