1use anyhow::{Context, Result, bail};
2use clap::Parser;
3use libtest_mimic::Trial;
4use std::borrow::Cow;
5use std::collections::{HashMap, HashSet};
6use std::fmt;
7use std::fs;
8use std::mem;
9use std::path::{Path, PathBuf};
10use std::process::{Command, Stdio};
11use std::sync::{Arc, Mutex};
12use wasm_encoder::{Encode, Section};
13use wit_component::{ComponentEncoder, StringEncoding};
14
15mod c;
16mod config;
17mod cpp;
18mod csharp;
19mod custom;
20mod go;
21mod moonbit;
22mod runner;
23mod rust;
24mod wat;
25
26#[derive(Default, Debug, Clone, Parser)]
68pub struct Opts {
69 test: Vec<PathBuf>,
71
72 #[clap(long, value_name = "PATH")]
74 artifacts: PathBuf,
75
76 #[clap(short, long, value_name = "REGEX")]
80 filter: Option<regex::Regex>,
81
82 #[clap(long, default_value = "wasmtime")]
84 runner: std::ffi::OsString,
85
86 #[clap(flatten)]
87 rust: rust::RustOpts,
88
89 #[clap(flatten)]
90 c: c::COpts,
91
92 #[clap(flatten)]
93 custom: custom::CustomOpts,
94
95 #[clap(short, long)]
101 inherit_stderr: bool,
102
103 #[clap(short, long, required = true, value_delimiter = ',')]
107 languages: Vec<String>,
108
109 #[clap(short, long, conflicts_with = "format")]
111 quiet: bool,
112
113 #[clap(long, value_name = "N")]
115 test_threads: Option<usize>,
116
117 #[clap(long)]
119 exact: bool,
120
121 #[clap(long, value_name = "FILTER")]
124 skip: Vec<String>,
125
126 #[clap(long, value_name = "auto|always|never")]
128 color: Option<libtest_mimic::ColorSetting>,
129
130 #[clap(long, value_name = "pretty|terse")]
132 format: Option<libtest_mimic::FormatSetting>,
133}
134
135impl Opts {
136 pub fn run(&self, wit_bindgen: &Path) -> Result<()> {
137 Runner {
138 opts: self.clone(),
139 rust_state: None,
140 wit_bindgen: wit_bindgen.to_path_buf(),
141 test_runner: runner::TestRunner::new(&self.runner)?,
142 }
143 .run()
144 }
145}
146
147#[derive(Clone)]
149struct Test {
150 name: String,
154
155 path: PathBuf,
157
158 config: config::WitConfig,
160
161 kind: TestKind,
162}
163
164#[derive(Clone)]
165enum TestKind {
166 Runtime(Vec<Component>),
167 Codegen(PathBuf),
168}
169
170#[derive(Clone)]
172struct Component {
173 name: String,
177
178 path: PathBuf,
180
181 kind: Kind,
183
184 language: Language,
186
187 bindgen: Bindgen,
190
191 contents: String,
193
194 lang_config: Option<HashMap<String, toml::Value>>,
196}
197
198#[derive(Clone)]
199struct Bindgen {
200 args: Vec<String>,
203 wit_path: PathBuf,
206 world: String,
209 wit_config: config::WitConfig,
211}
212
213#[derive(Debug, PartialEq, Copy, Clone)]
214enum Kind {
215 Runner,
216 Test,
217}
218
219#[derive(Clone, Debug, PartialEq, Eq, Hash)]
220enum Language {
221 Rust,
222 C,
223 Cpp,
224 Wat,
225 Csharp,
226 MoonBit,
227 Go,
228 Custom(custom::Language),
229}
230
231struct Compile<'a> {
234 component: &'a Component,
235 bindings_dir: &'a Path,
236 artifacts_dir: &'a Path,
237 output: &'a Path,
238}
239
240struct Verify<'a> {
243 wit_test: &'a Path,
244 bindings_dir: &'a Path,
245 artifacts_dir: &'a Path,
246 args: &'a [String],
247 world: &'a str,
248}
249
250struct Runner {
252 opts: Opts,
253 rust_state: Option<rust::State>,
254 wit_bindgen: PathBuf,
255 test_runner: runner::TestRunner,
256}
257
258impl Runner {
259 fn run(mut self) -> Result<()> {
261 let mut tests = HashMap::new();
263 for test in self.opts.test.iter() {
264 self.discover_tests(&mut tests, test)
265 .with_context(|| format!("failed to discover tests in {test:?}"))?;
266 }
267 if tests.is_empty() {
268 bail!(
269 "no `test.wit` files found were found in {:?}",
270 self.opts.test,
271 );
272 }
273
274 self.prepare_languages(&tests)?;
275 let me = Arc::new(self);
276 me.run_codegen_tests(&tests)?;
277 me.run_runtime_tests(&tests)?;
278
279 Ok(())
280 }
281
282 fn discover_tests(&self, tests: &mut HashMap<String, Test>, path: &Path) -> Result<()> {
284 if path.is_file() {
285 if path.extension().and_then(|s| s.to_str()) == Some("wit") {
286 let config =
287 fs::read_to_string(path).with_context(|| format!("failed to read {path:?}"))?;
288 let config = config::parse_test_config::<config::WitConfig>(&config, "//@")
289 .with_context(|| format!("failed to parse test config from {path:?}"))?;
290 return self.insert_test(&path, config, TestKind::Codegen(path.to_owned()), tests);
291 }
292
293 return Ok(());
294 }
295
296 let runtime_candidate = path.join("test.wit");
297 if runtime_candidate.is_file() {
298 let (config, components) = self
299 .load_runtime_test(&runtime_candidate, path)
300 .with_context(|| format!("failed to load test in {path:?}"))?;
301 return self.insert_test(path, config, TestKind::Runtime(components), tests);
302 }
303
304 let codegen_candidate = path.join("wit");
305 if codegen_candidate.is_dir() {
306 return self.insert_test(
307 path,
308 Default::default(),
309 TestKind::Codegen(codegen_candidate),
310 tests,
311 );
312 }
313
314 for entry in path.read_dir().context("failed to read test directory")? {
315 let entry = entry.context("failed to read test directory entry")?;
316 let path = entry.path();
317
318 self.discover_tests(tests, &path)?;
319 }
320
321 Ok(())
322 }
323
324 fn insert_test(
325 &self,
326 path: &Path,
327 config: config::WitConfig,
328 kind: TestKind,
329 tests: &mut HashMap<String, Test>,
330 ) -> Result<()> {
331 let test_name = path
332 .file_name()
333 .and_then(|s| s.to_str())
334 .context("non-utf-8 filename")?;
335 let prev = tests.insert(
336 test_name.to_string(),
337 Test {
338 name: test_name.to_string(),
339 path: path.to_path_buf(),
340 config,
341 kind,
342 },
343 );
344 if prev.is_some() {
345 bail!("duplicate test name `{test_name}` found");
346 }
347 Ok(())
348 }
349
350 fn load_runtime_test(
354 &self,
355 wit: &Path,
356 dir: &Path,
357 ) -> Result<(config::WitConfig, Vec<Component>)> {
358 let mut resolve = wit_parser::Resolve::default();
359
360 let wit_path = if dir.join("deps").exists() { dir } else { wit };
361 let (pkg, _files) = resolve.push_path(wit_path).context(format!(
362 "failed to load `test.wit` in test directory: {:?}",
363 &wit
364 ))?;
365 let resolve = Arc::new(resolve);
366
367 let wit_contents = std::fs::read_to_string(wit)?;
368 let wit_config: config::WitConfig = config::parse_test_config(&wit_contents, "//@")
369 .context("failed to parse WIT test config")?;
370
371 let mut worlds = Vec::new();
372
373 let mut push_world = |kind: Kind, name: &str| -> Result<()> {
374 let world = resolve.select_world(&[pkg], Some(name)).with_context(|| {
375 format!("failed to find expected `{name}` world to generate bindings")
376 })?;
377 worlds.push((world, kind));
378 Ok(())
379 };
380 push_world(Kind::Runner, wit_config.runner_world())?;
381 for world in wit_config.dependency_worlds() {
382 push_world(Kind::Test, &world)?;
383 }
384
385 let mut components = Vec::new();
386 let mut any_runner = false;
387 let mut any_test = false;
388
389 for entry in dir.read_dir().context("failed to read test directory")? {
390 let entry = entry.context("failed to read test directory entry")?;
391 let path = entry.path();
392
393 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
394 continue;
395 };
396 if name == "test.wit" {
397 continue;
398 }
399
400 let Some((world, kind)) = worlds
401 .iter()
402 .find(|(world, _kind)| name.starts_with(&resolve.worlds[*world].name))
403 else {
404 log::debug!("skipping file {name:?}");
405 continue;
406 };
407 match kind {
408 Kind::Runner => any_runner = true,
409 Kind::Test => any_test = true,
410 }
411 let bindgen = Bindgen {
412 args: Vec::new(),
413 wit_config: wit_config.clone(),
414 world: resolve.worlds[*world].name.clone(),
415 wit_path: wit_path.to_path_buf(),
416 };
417 let component = self
418 .parse_component(&path, *kind, bindgen)
419 .with_context(|| format!("failed to parse component source file {path:?}"))?;
420 components.push(component);
421 }
422
423 if !any_runner {
424 bail!("no runner files found in test directory");
425 }
426 if !any_test {
427 bail!("no test files found in test directory");
428 }
429
430 Ok((wit_config, components))
431 }
432
433 fn parse_component(&self, path: &Path, kind: Kind, mut bindgen: Bindgen) -> Result<Component> {
436 let extension = path
437 .extension()
438 .and_then(|s| s.to_str())
439 .context("non-utf-8 path extension")?;
440
441 let language = match extension {
442 "rs" => Language::Rust,
443 "c" => Language::C,
444 "cpp" => Language::Cpp,
445 "wat" => Language::Wat,
446 "cs" => Language::Csharp,
447 "mbt" => Language::MoonBit,
448 "go" => Language::Go,
449 other => Language::Custom(custom::Language::lookup(self, other)?),
450 };
451
452 let contents = fs::read_to_string(&path)?;
453 let config = match language.obj().comment_prefix_for_test_config() {
454 Some(comment) => {
455 config::parse_test_config::<config::RuntimeTestConfig>(&contents, comment)?
456 }
457 None => Default::default(),
458 };
459 assert!(bindgen.args.is_empty());
460 bindgen.args = config.args.into();
461
462 Ok(Component {
463 name: path.file_stem().unwrap().to_str().unwrap().to_string(),
464 path: path.to_path_buf(),
465 language,
466 bindgen,
467 kind,
468 contents,
469 lang_config: config.lang,
470 })
471 }
472
473 fn prepare_languages(&mut self, tests: &HashMap<String, Test>) -> Result<()> {
476 let all_languages = self.all_languages();
477
478 let mut prepared = HashSet::new();
479 let mut prepare = |lang: &Language| -> Result<()> {
480 if !self.include_language(lang) || !prepared.insert(lang.clone()) {
481 return Ok(());
482 }
483 lang.obj()
484 .prepare(self)
485 .with_context(|| format!("failed to prepare language {lang}"))
486 };
487
488 for test in tests.values() {
489 match &test.kind {
490 TestKind::Runtime(c) => {
491 for component in c {
492 prepare(&component.language)?
493 }
494 }
495 TestKind::Codegen(_) => {
496 for lang in all_languages.iter() {
497 prepare(lang)?;
498 }
499 }
500 }
501 }
502
503 Ok(())
504 }
505
506 fn all_languages(&self) -> Vec<Language> {
507 let mut languages = Language::ALL.to_vec();
508 for (ext, _) in self.opts.custom.custom.iter() {
509 languages.push(Language::Custom(
510 custom::Language::lookup(self, ext).unwrap(),
511 ));
512 }
513 languages
514 }
515
516 fn run_codegen_tests(self: &Arc<Self>, tests: &HashMap<String, Test>) -> Result<()> {
518 let mut codegen_tests = Vec::new();
519 let languages = self.all_languages();
520 for (name, config, test) in tests.iter().filter_map(|(name, t)| match &t.kind {
521 TestKind::Runtime(_) => None,
522 TestKind::Codegen(p) => Some((name, &t.config, p)),
523 }) {
524 if let Some(filter) = &self.opts.filter {
525 if !filter.is_match(name) {
526 continue;
527 }
528 }
529 for language in languages.iter() {
530 if !self.include_language(&language) {
533 continue;
534 }
535
536 let mut args = Vec::new();
537 for arg in language.obj().default_bindgen_args_for_codegen() {
538 args.push(arg.to_string());
539 }
540
541 codegen_tests.push((
542 language.clone(),
543 test.to_owned(),
544 name.to_string(),
545 args.clone(),
546 config.clone(),
547 ));
548
549 for (args_kind, new_args) in language.obj().codegen_test_variants() {
550 let mut args = args.clone();
551 for arg in new_args.iter() {
552 args.push(arg.to_string());
553 }
554 codegen_tests.push((
555 language.clone(),
556 test.clone(),
557 format!("{name}-{args_kind}"),
558 args,
559 config.clone(),
560 ));
561 }
562 }
563 }
564
565 if codegen_tests.is_empty() {
566 return Ok(());
567 }
568
569 println!("=== Running codegen tests ===");
570 self.run_tests(
571 codegen_tests
572 .into_iter()
573 .map(|(language, test, args_kind, args, config)| {
574 let me = self.clone();
575 let should_fail = language
576 .obj()
577 .should_fail_verify(&args_kind, &config, &args);
578
579 let name = format!("{language} {args_kind} {test:?}");
580 Trial::test(&name, move || {
581 let result = me
582 .codegen_test(&language, &test, &args_kind, &args, &config)
583 .with_context(|| {
584 format!("failed to codegen test for `{language}` over {test:?}")
585 });
586
587 me.render_error(
588 StepResult::new(result)
589 .should_fail(should_fail)
590 .metadata("language", language)
591 .metadata("variant", args_kind),
592 )
593 })
594 })
595 .collect::<Vec<_>>(),
596 );
597
598 Ok(())
599 }
600
601 fn run_tests(&self, trials: Vec<Trial>) {
602 let args = libtest_mimic::Arguments {
603 skip: self.opts.skip.clone(),
604 quiet: self.opts.quiet,
605 format: self.opts.format,
606 color: self.opts.color,
607 test_threads: self.opts.test_threads,
608 exact: self.opts.exact,
609 ..Default::default()
610 };
611 libtest_mimic::run(&args, trials).exit_if_failed();
612 }
613
614 fn codegen_test(
620 &self,
621 language: &Language,
622 test: &Path,
623 args_kind: &str,
624 args: &[String],
625 config: &config::WitConfig,
626 ) -> Result<()> {
627 let mut resolve = wit_parser::Resolve::default();
628 let (pkg, _) = resolve.push_path(test).context("failed to load WIT")?;
629 let world = resolve
630 .select_world(&[pkg], None)
631 .or_else(|err| {
632 resolve
633 .select_world(&[pkg], Some("imports"))
634 .map_err(|_| err)
635 })
636 .context("failed to select a world for bindings generation")?;
637 let world = resolve.worlds[world].name.clone();
638
639 let artifacts_dir = std::env::current_dir()?
640 .join(&self.opts.artifacts)
641 .join("codegen")
642 .join(language.to_string())
643 .join(args_kind);
644 let _ = fs::remove_dir_all(&artifacts_dir);
645 let bindings_dir = artifacts_dir.join("bindings");
646 let bindgen = Bindgen {
647 args: args.to_vec(),
648 wit_path: test.to_path_buf(),
649 world: world.clone(),
650 wit_config: config.clone(),
651 };
652 language
653 .obj()
654 .generate_bindings(self, &bindgen, &bindings_dir)
655 .context("failed to generate bindings")?;
656
657 language
658 .obj()
659 .verify(
660 self,
661 &Verify {
662 world: &world,
663 artifacts_dir: &artifacts_dir,
664 bindings_dir: &bindings_dir,
665 wit_test: test,
666 args: &bindgen.args,
667 },
668 )
669 .context("failed to verify generated bindings")?;
670
671 Ok(())
672 }
673
674 fn run_runtime_tests(self: &Arc<Self>, tests: &HashMap<String, Test>) -> Result<()> {
676 let components = tests
677 .values()
678 .filter(|t| match &self.opts.filter {
679 Some(filter) => filter.is_match(&t.name),
680 None => true,
681 })
682 .filter_map(|t| match &t.kind {
683 TestKind::Runtime(c) => Some(c.iter().map(move |c| (t, c))),
684 TestKind::Codegen(_) => None,
685 })
686 .flat_map(|i| i)
687 .filter(|(_test, component)| self.include_language(&component.language))
690 .collect::<Vec<_>>();
691
692 println!("=== Compiling components ===");
693 let compilations = Arc::new(Mutex::new(Vec::new()));
694 self.run_tests(
695 components
696 .into_iter()
697 .map(|(test, component)| {
698 let me = self.clone();
699 let compilations = compilations.clone();
700 let test = test.clone();
701 let component = component.clone();
702 Trial::test(&component.path.display().to_string(), move || {
703 let result = me.compile_component(&test, &component).with_context(|| {
704 format!("failed to compile component {:?}", component.path)
705 });
706 match result {
707 Ok(path) => {
708 compilations.lock().unwrap().push((test, component, path));
709 Ok(())
710 }
711 Err(e) => me.render_error(
712 StepResult::new(Err(e))
713 .metadata("component", &component.name)
714 .metadata("path", component.path.display()),
715 ),
716 }
717 })
718 })
719 .collect(),
720 );
721 let compilations = mem::take(&mut *compilations.lock().unwrap());
722
723 let mut compiled_components = HashMap::new();
728 for (test, component, path) in compilations {
729 let list = compiled_components.entry(test.name).or_insert(Vec::new());
730 list.push((component, path));
731 }
732
733 let mut to_run = Vec::new();
734 for (test, components) in compiled_components.iter() {
735 for a in components.iter().filter(|(c, _)| c.kind == Kind::Runner) {
736 self.push_tests(&tests[test.as_str()], components, a, &mut to_run)
737 .with_context(|| format!("failed to make test for `{test}`"))?;
738 }
739 }
740
741 println!("=== Running runtime tests ===");
742
743 self.run_tests(
744 to_run
745 .into_iter()
746 .map(|(case_name, (runner, runner_path), test_components)| {
747 let me = self.clone();
748 let mut name = format!("{case_name}");
749 for component in [&runner]
750 .into_iter()
751 .chain(test_components.iter().map(|p| &p.0))
752 {
753 name.push_str(&format!(
754 " | {}",
755 component.path.file_name().unwrap().to_str().unwrap()
756 ));
757 }
758 let case_name = case_name.to_string();
759 let runner = runner.clone();
760 let runner_path = runner_path.to_path_buf();
761 let case = tests[case_name.as_str()].clone();
762 Trial::test(&name, move || {
763 let result = me
764 .runtime_test(&case, &runner, &runner_path, &test_components)
765 .with_context(|| format!("failed to run `{}`", case.name));
766 me.render_error(
767 StepResult::new(result)
768 .metadata("runner", runner.path.display())
769 .metadata("compiled runner", runner_path.display()),
770 )
771 })
772 })
773 .collect(),
774 );
775
776 Ok(())
777 }
778
779 fn push_tests(
782 &self,
783 test: &Test,
784 components: &[(Component, PathBuf)],
785 runner: &(Component, PathBuf),
786 to_run: &mut Vec<(String, (Component, PathBuf), Vec<(Component, PathBuf)>)>,
787 ) -> Result<()> {
788 fn push(
796 worlds: &[String],
797 components: &[(Component, PathBuf)],
798 test: &mut Vec<(Component, PathBuf)>,
799 commit: &mut dyn FnMut(Vec<(Component, PathBuf)>),
800 ) -> Result<()> {
801 match worlds.split_first() {
802 Some((world, rest)) => {
803 let mut any = false;
804 for (component, path) in components {
805 if component.bindgen.world == *world {
806 any = true;
807 test.push((component.clone(), path.clone()));
808 push(rest, components, test, commit)?;
809 test.pop();
810 }
811 }
812 if !any {
813 bail!("no components found for `{world}`");
814 }
815 }
816
817 None => commit(test.clone()),
819 }
820 Ok(())
821 }
822
823 push(
824 &test.config.dependency_worlds(),
825 components,
826 &mut Vec::new(),
827 &mut |test_components| {
828 to_run.push((
829 test.name.clone(),
830 (runner.0.clone(), runner.1.clone()),
831 test_components,
832 ));
833 },
834 )
835 }
836
837 fn compile_component(&self, test: &Test, component: &Component) -> Result<PathBuf> {
842 let root_dir = std::env::current_dir()?
843 .join(&self.opts.artifacts)
844 .join(&test.name);
845 let artifacts_dir = root_dir.join(format!("{}-{}", component.name, component.language));
846 let _ = fs::remove_dir_all(&artifacts_dir);
847 let bindings_dir = artifacts_dir.join("bindings");
848 let output = root_dir.join(format!("{}-{}.wasm", component.name, component.language));
849 component
850 .language
851 .obj()
852 .generate_bindings(self, &component.bindgen, &bindings_dir)?;
853 let result = Compile {
854 component,
855 bindings_dir: &bindings_dir,
856 artifacts_dir: &artifacts_dir,
857 output: &output,
858 };
859 component.language.obj().compile(self, &result)?;
860
861 let wasm = fs::read(&output)
863 .with_context(|| format!("failed to read output wasm file {output:?}"))?;
864 if !wasmparser::Parser::is_component(&wasm) {
865 bail!("output file {output:?} is not a component");
866 }
867 wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all())
868 .validate_all(&wasm)
869 .with_context(|| {
870 format!(
871 "compiler produced invalid wasm file {output:?} for component {}",
872 component.name
873 )
874 })?;
875
876 Ok(output)
877 }
878
879 fn runtime_test(
884 &self,
885 case: &Test,
886 runner: &Component,
887 runner_wasm: &Path,
888 test_components: &[(Component, PathBuf)],
889 ) -> Result<()> {
890 let composed = if case.config.wac.is_none() {
896 self.compose_wasm_with_wasm_compose(runner_wasm, test_components)?
897 } else {
898 self.compose_wasm_with_wac(case, runner, runner_wasm, test_components)?
899 };
900
901 let dst = runner_wasm.parent().unwrap();
902 let mut filename = format!(
903 "composed-{}",
904 runner.path.file_name().unwrap().to_str().unwrap(),
905 );
906 for (test, _) in test_components {
907 filename.push_str("-");
908 filename.push_str(test.path.file_name().unwrap().to_str().unwrap());
909 }
910 filename.push_str(".wasm");
911 let composed_wasm = dst.join(filename);
912 write_if_different(&composed_wasm, &composed)?;
913
914 self.run_command(self.test_runner.command().arg(&composed_wasm))?;
915 Ok(())
916 }
917
918 fn compose_wasm_with_wasm_compose(
919 &self,
920 runner_wasm: &Path,
921 test_components: &[(Component, PathBuf)],
922 ) -> Result<Vec<u8>> {
923 assert!(test_components.len() > 0);
924 let mut last_bytes = None;
925 let mut path: PathBuf;
926 for (i, (_component, component_path)) in test_components.iter().enumerate() {
927 let main = match last_bytes.take() {
928 Some(bytes) => {
929 path = runner_wasm.with_extension(&format!("composition{i}.wasm"));
930 std::fs::write(&path, &bytes)
931 .with_context(|| format!("failed to write temporary file {path:?}"))?;
932 path.as_path()
933 }
934 None => runner_wasm,
935 };
936
937 let mut config = wasm_compose::config::Config::default();
938 config.definitions = vec![component_path.to_path_buf()];
939 last_bytes = Some(
940 wasm_compose::composer::ComponentComposer::new(main, &config)
941 .compose()
942 .with_context(|| {
943 format!("failed to compose {main:?} with {component_path:?}")
944 })?,
945 );
946 }
947
948 Ok(last_bytes.unwrap())
949 }
950
951 fn compose_wasm_with_wac(
952 &self,
953 case: &Test,
954 runner: &Component,
955 runner_wasm: &Path,
956 test_components: &[(Component, PathBuf)],
957 ) -> Result<Vec<u8>> {
958 let document = match &case.config.wac {
959 Some(path) => {
960 let wac_config = case.path.join(path);
961 fs::read_to_string(&wac_config)
962 .with_context(|| format!("failed to read {wac_config:?}"))?
963 }
964 None => {
967 let mut script = String::from("package example:composition;\n");
968 let mut args = Vec::new();
969 for (component, _path) in test_components {
970 let world = &component.bindgen.world;
971 args.push(format!("...{world}"));
972 script.push_str(&format!("let {world} = new test:{world} {{ ... }};\n"));
973 }
974 args.push("...".to_string());
975 let runner = &runner.bindgen.world;
976 script.push_str(&format!(
977 "let runner = new test:{runner} {{ {} }};\n\
978 export runner...;",
979 args.join(", ")
980 ));
981
982 script
983 }
984 };
985
986 let components_as_packages = test_components
989 .iter()
990 .map(|(component, path)| {
991 Ok((format!("test:{}", component.bindgen.world), fs::read(path)?))
992 })
993 .collect::<Result<Vec<_>>>()?;
994
995 let runner_name = format!("test:{}", runner.bindgen.world);
996 let mut packages = indexmap::IndexMap::new();
997 packages.insert(
998 wac_types::BorrowedPackageKey {
999 name: &runner_name,
1000 version: None,
1001 },
1002 fs::read(runner_wasm)?,
1003 );
1004 for (name, contents) in components_as_packages.iter() {
1005 packages.insert(
1006 wac_types::BorrowedPackageKey {
1007 name,
1008 version: None,
1009 },
1010 contents.clone(),
1011 );
1012 }
1013
1014 let document =
1016 wac_parser::Document::parse(&document).context("failed to parse wac script")?;
1017 document
1018 .resolve(packages)
1019 .context("failed to run `wac` resolve")?
1020 .encode(wac_graph::EncodeOptions {
1021 define_components: true,
1022 validate: false,
1023 processor: None,
1024 })
1025 .context("failed to encode `wac` result")
1026 }
1027
1028 fn run_command(&self, cmd: &mut Command) -> Result<()> {
1031 if self.opts.inherit_stderr {
1032 cmd.stderr(Stdio::inherit());
1033 }
1034 let output = cmd
1035 .output()
1036 .with_context(|| format!("failed to spawn {cmd:?}"))?;
1037 if output.status.success() {
1038 return Ok(());
1039 }
1040
1041 let mut error = format!(
1042 "\
1043command execution failed
1044command: {cmd:?}
1045status: {}",
1046 output.status,
1047 );
1048
1049 if !output.stdout.is_empty() {
1050 error.push_str(&format!(
1051 "\nstdout:\n {}",
1052 String::from_utf8_lossy(&output.stdout).replace("\n", "\n ")
1053 ));
1054 }
1055 if !output.stderr.is_empty() {
1056 error.push_str(&format!(
1057 "\nstderr:\n {}",
1058 String::from_utf8_lossy(&output.stderr).replace("\n", "\n ")
1059 ));
1060 }
1061
1062 bail!("{error}")
1063 }
1064
1065 fn convert_p1_to_component(&self, p1: &Path, compile: &Compile<'_>) -> Result<()> {
1070 let mut resolve = wit_parser::Resolve::default();
1071 let (pkg, _) = resolve
1072 .push_path(&compile.component.bindgen.wit_path)
1073 .context("failed to load WIT")?;
1074 let world = resolve.select_world(&[pkg], Some(&compile.component.bindgen.world))?;
1075 let mut module = fs::read(&p1).context("failed to read wasm file")?;
1076
1077 if !has_component_type_sections(&module) {
1078 let encoded =
1079 wit_component::metadata::encode(&resolve, world, StringEncoding::UTF8, None)?;
1080 let section = wasm_encoder::CustomSection {
1081 name: Cow::Borrowed("component-type"),
1082 data: Cow::Borrowed(&encoded),
1083 };
1084 module.push(section.id());
1085 section.encode(&mut module);
1086 }
1087
1088 let wasi_adapter =
1089 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
1090
1091 let component = ComponentEncoder::default()
1092 .module(module.as_slice())
1093 .context("failed to load custom sections from input module")?
1094 .validate(true)
1095 .adapter("wasi_snapshot_preview1", wasi_adapter)
1096 .context("failed to load wasip1 adapter")?
1097 .encode()
1098 .context("failed to convert to a component")?;
1099 write_if_different(compile.output, component)?;
1100 Ok(())
1101 }
1102
1103 fn include_language(&self, language: &Language) -> bool {
1105 self.opts
1106 .languages
1107 .iter()
1108 .any(|l| l == language.obj().display())
1109 }
1110
1111 fn render_error(&self, result: StepResult<'_>) -> Result<(), libtest_mimic::Failed> {
1112 let err = match (result.result, result.should_fail) {
1113 (Ok(()), false) | (Err(_), true) => return Ok(()),
1114 (Err(e), false) => e,
1115 (Ok(()), true) => return Err("test should have failed, but passed".into()),
1116 };
1117
1118 let mut s = String::new();
1119 for (k, v) in result.metadata {
1120 s.push_str(&format!(" {k}: {v}\n"));
1121 }
1122 s.push_str(&format!(
1123 " error: {}",
1124 format!("{err:?}").replace("\n", "\n ")
1125 ));
1126 Err(s.into())
1127 }
1128}
1129
1130fn has_component_type_sections(wasm: &[u8]) -> bool {
1131 for payload in wasmparser::Parser::new(0).parse_all(wasm) {
1132 match payload {
1133 Ok(wasmparser::Payload::CustomSection(s)) if s.name().starts_with("component-type") => {
1134 return true;
1135 }
1136 _ => {}
1137 }
1138 }
1139 false
1140}
1141
1142struct StepResult<'a> {
1143 result: Result<()>,
1144 should_fail: bool,
1145 metadata: Vec<(&'a str, String)>,
1146}
1147
1148impl<'a> StepResult<'a> {
1149 fn new(result: Result<()>) -> StepResult<'a> {
1150 StepResult {
1151 result,
1152 should_fail: false,
1153 metadata: Vec::new(),
1154 }
1155 }
1156
1157 fn should_fail(mut self, fail: bool) -> Self {
1158 self.should_fail = fail;
1159 self
1160 }
1161
1162 fn metadata(mut self, name: &'a str, value: impl fmt::Display) -> Self {
1163 self.metadata.push((name, value.to_string()));
1164 self
1165 }
1166}
1167
1168trait LanguageMethods {
1171 fn display(&self) -> &str;
1173
1174 fn comment_prefix_for_test_config(&self) -> Option<&str>;
1180
1181 fn codegen_test_variants(&self) -> &[(&str, &[&str])] {
1190 &[]
1191 }
1192
1193 fn prepare(&self, runner: &mut Runner) -> Result<()>;
1196
1197 fn generate_bindings_prepare(
1199 &self,
1200 _runner: &Runner,
1201 _bindgen: &Bindgen,
1202 _dir: &Path,
1203 ) -> Result<()> {
1204 Ok(())
1205 }
1206
1207 fn generate_bindings(&self, runner: &Runner, bindgen: &Bindgen, dir: &Path) -> Result<()> {
1211 let name = match self.bindgen_name() {
1212 Some(name) => name,
1213 None => return Ok(()),
1214 };
1215 self.generate_bindings_prepare(runner, bindgen, dir)?;
1216 let mut cmd = Command::new(&runner.wit_bindgen);
1217 cmd.arg(name)
1218 .arg(&bindgen.wit_path)
1219 .arg("--world")
1220 .arg(format!("%{}", bindgen.world))
1221 .arg("--out-dir")
1222 .arg(dir);
1223
1224 match bindgen.wit_config.default_bindgen_args {
1225 Some(true) | None => {
1226 for arg in self.default_bindgen_args() {
1227 cmd.arg(arg);
1228 }
1229 }
1230 Some(false) => {}
1231 }
1232
1233 for arg in bindgen.args.iter() {
1234 cmd.arg(arg);
1235 }
1236
1237 runner.run_command(&mut cmd)
1238 }
1239
1240 fn default_bindgen_args(&self) -> &[&str] {
1245 &[]
1246 }
1247
1248 fn default_bindgen_args_for_codegen(&self) -> &[&str] {
1251 &[]
1252 }
1253
1254 fn bindgen_name(&self) -> Option<&str> {
1261 Some(self.display())
1262 }
1263
1264 fn compile(&self, runner: &Runner, compile: &Compile) -> Result<()>;
1266
1267 fn should_fail_verify(&self, name: &str, config: &config::WitConfig, args: &[String]) -> bool;
1270
1271 fn verify(&self, runner: &Runner, verify: &Verify) -> Result<()>;
1274}
1275
1276impl Language {
1277 const ALL: &[Language] = &[
1278 Language::Rust,
1279 Language::C,
1280 Language::Cpp,
1281 Language::Wat,
1282 Language::Csharp,
1283 Language::MoonBit,
1284 Language::Go,
1285 ];
1286
1287 fn obj(&self) -> &dyn LanguageMethods {
1288 match self {
1289 Language::Rust => &rust::Rust,
1290 Language::C => &c::C,
1291 Language::Cpp => &cpp::Cpp,
1292 Language::Wat => &wat::Wat,
1293 Language::Csharp => &csharp::Csharp,
1294 Language::MoonBit => &moonbit::MoonBit,
1295 Language::Go => &go::Go,
1296 Language::Custom(custom) => custom,
1297 }
1298 }
1299}
1300
1301impl fmt::Display for Language {
1302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1303 self.obj().display().fmt(f)
1304 }
1305}
1306
1307impl fmt::Display for Kind {
1308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1309 match self {
1310 Kind::Runner => "runner".fmt(f),
1311 Kind::Test => "test".fmt(f),
1312 }
1313 }
1314}
1315
1316fn write_if_different(path: &Path, contents: impl AsRef<[u8]>) -> Result<bool> {
1319 let contents = contents.as_ref();
1320 if let Ok(prev) = fs::read(path) {
1321 if prev == contents {
1322 return Ok(false);
1323 }
1324 }
1325
1326 if let Some(parent) = path.parent() {
1327 fs::create_dir_all(parent)
1328 .with_context(|| format!("failed to create directory {parent:?}"))?;
1329 }
1330 fs::write(path, contents).with_context(|| format!("failed to write {path:?}"))?;
1331 Ok(true)
1332}
1333
1334impl Component {
1335 fn deserialize_lang_config<T>(&self) -> Result<T>
1341 where
1342 T: Default + serde::de::DeserializeOwned,
1343 {
1344 if self.lang_config.is_none() {
1347 return Ok(T::default());
1348 }
1349
1350 let config = config::parse_test_config::<config::RuntimeTestConfig<T>>(
1355 &self.contents,
1356 self.language
1357 .obj()
1358 .comment_prefix_for_test_config()
1359 .unwrap(),
1360 )?;
1361 Ok(config.lang.unwrap())
1362 }
1363}