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 wasmtime_flags: config::StringList,
199}
200
201#[derive(Clone)]
202struct Bindgen {
203 args: Vec<String>,
206 wit_path: PathBuf,
209 world: String,
212 wit_config: config::WitConfig,
214}
215
216#[derive(Debug, PartialEq, Copy, Clone)]
217enum Kind {
218 Runner,
219 Test,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Hash)]
223enum Language {
224 Rust,
225 C,
226 Cpp,
227 Wat,
228 Csharp,
229 MoonBit,
230 Go,
231 Custom(custom::Language),
232}
233
234struct Compile<'a> {
237 component: &'a Component,
238 bindings_dir: &'a Path,
239 artifacts_dir: &'a Path,
240 output: &'a Path,
241}
242
243struct Verify<'a> {
246 wit_test: &'a Path,
247 bindings_dir: &'a Path,
248 artifacts_dir: &'a Path,
249 args: &'a [String],
250 world: &'a str,
251}
252
253struct Runner {
255 opts: Opts,
256 rust_state: Option<rust::State>,
257 wit_bindgen: PathBuf,
258 test_runner: runner::TestRunner,
259}
260
261impl Runner {
262 fn run(mut self) -> Result<()> {
264 let mut tests = HashMap::new();
266 for test in self.opts.test.iter() {
267 self.discover_tests(&mut tests, test)
268 .with_context(|| format!("failed to discover tests in {test:?}"))?;
269 }
270 if tests.is_empty() {
271 bail!(
272 "no `test.wit` files found were found in {:?}",
273 self.opts.test,
274 );
275 }
276
277 self.prepare_languages(&tests)?;
278 let me = Arc::new(self);
279 me.run_codegen_tests(&tests)?;
280 me.run_runtime_tests(&tests)?;
281
282 Ok(())
283 }
284
285 fn discover_tests(&self, tests: &mut HashMap<String, Test>, path: &Path) -> Result<()> {
287 if path.is_file() {
288 if path.extension().and_then(|s| s.to_str()) == Some("wit") {
289 let config =
290 fs::read_to_string(path).with_context(|| format!("failed to read {path:?}"))?;
291 let config = config::parse_test_config::<config::WitConfig>(&config, "//@")
292 .with_context(|| format!("failed to parse test config from {path:?}"))?;
293 return self.insert_test(&path, config, TestKind::Codegen(path.to_owned()), tests);
294 }
295
296 return Ok(());
297 }
298
299 let runtime_candidate = path.join("test.wit");
300 if runtime_candidate.is_file() {
301 let (config, components) = self
302 .load_runtime_test(&runtime_candidate, path)
303 .with_context(|| format!("failed to load test in {path:?}"))?;
304 return self.insert_test(path, config, TestKind::Runtime(components), tests);
305 }
306
307 let codegen_candidate = path.join("wit");
308 if codegen_candidate.is_dir() {
309 return self.insert_test(
310 path,
311 Default::default(),
312 TestKind::Codegen(codegen_candidate),
313 tests,
314 );
315 }
316
317 for entry in path.read_dir().context("failed to read test directory")? {
318 let entry = entry.context("failed to read test directory entry")?;
319 let path = entry.path();
320
321 self.discover_tests(tests, &path)?;
322 }
323
324 Ok(())
325 }
326
327 fn insert_test(
328 &self,
329 path: &Path,
330 config: config::WitConfig,
331 kind: TestKind,
332 tests: &mut HashMap<String, Test>,
333 ) -> Result<()> {
334 let test_name = path
335 .file_name()
336 .and_then(|s| s.to_str())
337 .context("non-utf-8 filename")?;
338 let prev = tests.insert(
339 test_name.to_string(),
340 Test {
341 name: test_name.to_string(),
342 path: path.to_path_buf(),
343 config,
344 kind,
345 },
346 );
347 if prev.is_some() {
348 bail!("duplicate test name `{test_name}` found");
349 }
350 Ok(())
351 }
352
353 fn load_runtime_test(
357 &self,
358 wit: &Path,
359 dir: &Path,
360 ) -> Result<(config::WitConfig, Vec<Component>)> {
361 let mut resolve = wit_parser::Resolve::default();
362
363 let wit_path = if dir.join("deps").exists() { dir } else { wit };
364 let (pkg, _files) = resolve.push_path(wit_path).context(format!(
365 "failed to load `test.wit` in test directory: {:?}",
366 &wit
367 ))?;
368 let resolve = Arc::new(resolve);
369
370 let wit_contents = std::fs::read_to_string(wit)?;
371 let wit_config: config::WitConfig = config::parse_test_config(&wit_contents, "//@")
372 .context("failed to parse WIT test config")?;
373
374 let mut worlds = Vec::new();
375
376 let mut push_world = |kind: Kind, name: &str| -> Result<()> {
377 let world = resolve.select_world(&[pkg], Some(name)).with_context(|| {
378 format!("failed to find expected `{name}` world to generate bindings")
379 })?;
380 worlds.push((world, kind));
381 Ok(())
382 };
383 push_world(Kind::Runner, wit_config.runner_world())?;
384 for world in wit_config.dependency_worlds() {
385 push_world(Kind::Test, &world)?;
386 }
387
388 let mut components = Vec::new();
389 let mut any_runner = false;
390 let mut any_test = false;
391
392 for entry in dir.read_dir().context("failed to read test directory")? {
393 let entry = entry.context("failed to read test directory entry")?;
394 let path = entry.path();
395
396 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
397 continue;
398 };
399 if name == "test.wit" {
400 continue;
401 }
402
403 let Some((world, kind)) = worlds
404 .iter()
405 .find(|(world, _kind)| name.starts_with(&resolve.worlds[*world].name))
406 else {
407 log::debug!("skipping file {name:?}");
408 continue;
409 };
410 match kind {
411 Kind::Runner => any_runner = true,
412 Kind::Test => any_test = true,
413 }
414 let bindgen = Bindgen {
415 args: Vec::new(),
416 wit_config: wit_config.clone(),
417 world: resolve.worlds[*world].name.clone(),
418 wit_path: wit_path.to_path_buf(),
419 };
420 let component = self
421 .parse_component(&path, *kind, bindgen)
422 .with_context(|| format!("failed to parse component source file {path:?}"))?;
423 components.push(component);
424 }
425
426 if !any_runner {
427 bail!("no runner files found in test directory");
428 }
429 if !any_test {
430 bail!("no test files found in test directory");
431 }
432
433 Ok((wit_config, components))
434 }
435
436 fn parse_component(&self, path: &Path, kind: Kind, mut bindgen: Bindgen) -> Result<Component> {
439 let extension = path
440 .extension()
441 .and_then(|s| s.to_str())
442 .context("non-utf-8 path extension")?;
443
444 let language = match extension {
445 "rs" => Language::Rust,
446 "c" => Language::C,
447 "cpp" => Language::Cpp,
448 "wat" => Language::Wat,
449 "cs" => Language::Csharp,
450 "mbt" => Language::MoonBit,
451 "go" => Language::Go,
452 other => Language::Custom(custom::Language::lookup(self, other)?),
453 };
454
455 let contents = fs::read_to_string(&path)?;
456 let config = match language.obj().comment_prefix_for_test_config() {
457 Some(comment) => {
458 config::parse_test_config::<config::RuntimeTestConfig>(&contents, comment)?
459 }
460 None => Default::default(),
461 };
462 assert!(bindgen.args.is_empty());
463 bindgen.args = config.args.into();
464
465 Ok(Component {
466 name: path.file_stem().unwrap().to_str().unwrap().to_string(),
467 path: path.to_path_buf(),
468 language,
469 bindgen,
470 kind,
471 contents,
472 lang_config: config.lang,
473 wasmtime_flags: config.wasmtime_flags,
474 })
475 }
476
477 fn prepare_languages(&mut self, tests: &HashMap<String, Test>) -> Result<()> {
480 let all_languages = self.all_languages();
481
482 let mut prepared = HashSet::new();
483 let mut prepare = |lang: &Language| -> Result<()> {
484 if !self.include_language(lang) || !prepared.insert(lang.clone()) {
485 return Ok(());
486 }
487 lang.obj()
488 .prepare(self)
489 .with_context(|| format!("failed to prepare language {lang}"))
490 };
491
492 for test in tests.values() {
493 match &test.kind {
494 TestKind::Runtime(c) => {
495 for component in c {
496 prepare(&component.language)?
497 }
498 }
499 TestKind::Codegen(_) => {
500 for lang in all_languages.iter() {
501 prepare(lang)?;
502 }
503 }
504 }
505 }
506
507 Ok(())
508 }
509
510 fn all_languages(&self) -> Vec<Language> {
511 let mut languages = Language::ALL.to_vec();
512 for (ext, _) in self.opts.custom.custom.iter() {
513 languages.push(Language::Custom(
514 custom::Language::lookup(self, ext).unwrap(),
515 ));
516 }
517 languages
518 }
519
520 fn run_codegen_tests(self: &Arc<Self>, tests: &HashMap<String, Test>) -> Result<()> {
522 let mut codegen_tests = Vec::new();
523 let languages = self.all_languages();
524 for (name, config, test) in tests.iter().filter_map(|(name, t)| match &t.kind {
525 TestKind::Runtime(_) => None,
526 TestKind::Codegen(p) => Some((name, &t.config, p)),
527 }) {
528 if let Some(filter) = &self.opts.filter {
529 if !filter.is_match(name) {
530 continue;
531 }
532 }
533 for language in languages.iter() {
534 if !self.include_language(&language) {
537 continue;
538 }
539
540 let mut args = Vec::new();
541 for arg in language.obj().default_bindgen_args_for_codegen() {
542 args.push(arg.to_string());
543 }
544
545 codegen_tests.push((
546 language.clone(),
547 test.to_owned(),
548 name.to_string(),
549 args.clone(),
550 config.clone(),
551 ));
552
553 for (args_kind, new_args) in language.obj().codegen_test_variants() {
554 let mut args = args.clone();
555 for arg in new_args.iter() {
556 args.push(arg.to_string());
557 }
558 codegen_tests.push((
559 language.clone(),
560 test.clone(),
561 format!("{name}-{args_kind}"),
562 args,
563 config.clone(),
564 ));
565 }
566 }
567 }
568
569 if codegen_tests.is_empty() {
570 return Ok(());
571 }
572
573 println!("=== Running codegen tests ===");
574 self.run_tests(
575 codegen_tests
576 .into_iter()
577 .map(|(language, test, args_kind, args, config)| {
578 let me = self.clone();
579 let should_fail = language
580 .obj()
581 .should_fail_verify(&args_kind, &config, &args);
582
583 let name = format!("{language} {args_kind} {test:?}");
584 Trial::test(&name, move || {
585 let result = me
586 .codegen_test(&language, &test, &args_kind, &args, &config)
587 .with_context(|| {
588 format!("failed to codegen test for `{language}` over {test:?}")
589 });
590
591 me.render_error(
592 StepResult::new(result)
593 .should_fail(should_fail)
594 .metadata("language", language)
595 .metadata("variant", args_kind),
596 )
597 })
598 })
599 .collect::<Vec<_>>(),
600 );
601
602 Ok(())
603 }
604
605 fn run_tests(&self, trials: Vec<Trial>) {
606 let args = libtest_mimic::Arguments {
607 skip: self.opts.skip.clone(),
608 quiet: self.opts.quiet,
609 format: self.opts.format,
610 color: self.opts.color,
611 test_threads: self.opts.test_threads,
612 exact: self.opts.exact,
613 ..Default::default()
614 };
615 libtest_mimic::run(&args, trials).exit_if_failed();
616 }
617
618 fn codegen_test(
624 &self,
625 language: &Language,
626 test: &Path,
627 args_kind: &str,
628 args: &[String],
629 config: &config::WitConfig,
630 ) -> Result<()> {
631 let mut resolve = wit_parser::Resolve::default();
632 let (pkg, _) = resolve.push_path(test).context("failed to load WIT")?;
633 let world = resolve
634 .select_world(&[pkg], None)
635 .or_else(|err| {
636 resolve
637 .select_world(&[pkg], Some("imports"))
638 .map_err(|_| err)
639 })
640 .context("failed to select a world for bindings generation")?;
641 let world = resolve.worlds[world].name.clone();
642
643 let artifacts_dir = std::env::current_dir()?
644 .join(&self.opts.artifacts)
645 .join("codegen")
646 .join(language.to_string())
647 .join(args_kind);
648 let _ = fs::remove_dir_all(&artifacts_dir);
649 let bindings_dir = artifacts_dir.join("bindings");
650 let bindgen = Bindgen {
651 args: args.to_vec(),
652 wit_path: test.to_path_buf(),
653 world: world.clone(),
654 wit_config: config.clone(),
655 };
656 language
657 .obj()
658 .generate_bindings(self, &bindgen, &bindings_dir)
659 .context("failed to generate bindings")?;
660
661 language
662 .obj()
663 .verify(
664 self,
665 &Verify {
666 world: &world,
667 artifacts_dir: &artifacts_dir,
668 bindings_dir: &bindings_dir,
669 wit_test: test,
670 args: &bindgen.args,
671 },
672 )
673 .context("failed to verify generated bindings")?;
674
675 Ok(())
676 }
677
678 fn run_runtime_tests(self: &Arc<Self>, tests: &HashMap<String, Test>) -> Result<()> {
680 let components = tests
681 .values()
682 .filter(|t| match &self.opts.filter {
683 Some(filter) => filter.is_match(&t.name),
684 None => true,
685 })
686 .filter_map(|t| match &t.kind {
687 TestKind::Runtime(c) => Some(c.iter().map(move |c| (t, c))),
688 TestKind::Codegen(_) => None,
689 })
690 .flat_map(|i| i)
691 .filter(|(_test, component)| self.include_language(&component.language))
694 .collect::<Vec<_>>();
695
696 println!("=== Compiling components ===");
697 let compilations = Arc::new(Mutex::new(Vec::new()));
698 self.run_tests(
699 components
700 .into_iter()
701 .map(|(test, component)| {
702 let me = self.clone();
703 let compilations = compilations.clone();
704 let test = test.clone();
705 let component = component.clone();
706 Trial::test(&component.path.display().to_string(), move || {
707 let result = me.compile_component(&test, &component).with_context(|| {
708 format!("failed to compile component {:?}", component.path)
709 });
710 match result {
711 Ok(path) => {
712 compilations.lock().unwrap().push((test, component, path));
713 Ok(())
714 }
715 Err(e) => me.render_error(
716 StepResult::new(Err(e))
717 .metadata("component", &component.name)
718 .metadata("path", component.path.display()),
719 ),
720 }
721 })
722 })
723 .collect(),
724 );
725 let compilations = mem::take(&mut *compilations.lock().unwrap());
726
727 let mut compiled_components = HashMap::new();
732 for (test, component, path) in compilations {
733 let list = compiled_components.entry(test.name).or_insert(Vec::new());
734 list.push((component, path));
735 }
736
737 let mut to_run = Vec::new();
738 for (test, components) in compiled_components.iter() {
739 for a in components.iter().filter(|(c, _)| c.kind == Kind::Runner) {
740 self.push_tests(&tests[test.as_str()], components, a, &mut to_run)
741 .with_context(|| format!("failed to make test for `{test}`"))?;
742 }
743 }
744
745 println!("=== Running runtime tests ===");
746
747 self.run_tests(
748 to_run
749 .into_iter()
750 .map(|(case_name, (runner, runner_path), test_components)| {
751 let me = self.clone();
752 let mut name = format!("{case_name}");
753 for component in [&runner]
754 .into_iter()
755 .chain(test_components.iter().map(|p| &p.0))
756 {
757 name.push_str(&format!(
758 " | {}",
759 component.path.file_name().unwrap().to_str().unwrap()
760 ));
761 }
762 let case_name = case_name.to_string();
763 let runner = runner.clone();
764 let runner_path = runner_path.to_path_buf();
765 let case = tests[case_name.as_str()].clone();
766 Trial::test(&name, move || {
767 let result = me
768 .runtime_test(&case, &runner, &runner_path, &test_components)
769 .with_context(|| format!("failed to run `{}`", case.name));
770 me.render_error(
771 StepResult::new(result)
772 .metadata("runner", runner.path.display())
773 .metadata("compiled runner", runner_path.display()),
774 )
775 })
776 })
777 .collect(),
778 );
779
780 Ok(())
781 }
782
783 fn push_tests(
786 &self,
787 test: &Test,
788 components: &[(Component, PathBuf)],
789 runner: &(Component, PathBuf),
790 to_run: &mut Vec<(String, (Component, PathBuf), Vec<(Component, PathBuf)>)>,
791 ) -> Result<()> {
792 fn push(
800 worlds: &[String],
801 components: &[(Component, PathBuf)],
802 test: &mut Vec<(Component, PathBuf)>,
803 commit: &mut dyn FnMut(Vec<(Component, PathBuf)>),
804 ) -> Result<()> {
805 match worlds.split_first() {
806 Some((world, rest)) => {
807 let mut any = false;
808 for (component, path) in components {
809 if component.bindgen.world == *world {
810 any = true;
811 test.push((component.clone(), path.clone()));
812 push(rest, components, test, commit)?;
813 test.pop();
814 }
815 }
816 if !any {
817 bail!("no components found for `{world}`");
818 }
819 }
820
821 None => commit(test.clone()),
823 }
824 Ok(())
825 }
826
827 push(
828 &test.config.dependency_worlds(),
829 components,
830 &mut Vec::new(),
831 &mut |test_components| {
832 to_run.push((
833 test.name.clone(),
834 (runner.0.clone(), runner.1.clone()),
835 test_components,
836 ));
837 },
838 )
839 }
840
841 fn compile_component(&self, test: &Test, component: &Component) -> Result<PathBuf> {
846 let root_dir = std::env::current_dir()?
847 .join(&self.opts.artifacts)
848 .join(&test.name);
849 let artifacts_dir = root_dir.join(format!("{}-{}", component.name, component.language));
850 let _ = fs::remove_dir_all(&artifacts_dir);
851 let bindings_dir = artifacts_dir.join("bindings");
852 let output = root_dir.join(format!("{}-{}.wasm", component.name, component.language));
853 component
854 .language
855 .obj()
856 .generate_bindings(self, &component.bindgen, &bindings_dir)?;
857 let result = Compile {
858 component,
859 bindings_dir: &bindings_dir,
860 artifacts_dir: &artifacts_dir,
861 output: &output,
862 };
863 component.language.obj().compile(self, &result)?;
864
865 let wasm = fs::read(&output)
867 .with_context(|| format!("failed to read output wasm file {output:?}"))?;
868 if !wasmparser::Parser::is_component(&wasm) {
869 bail!("output file {output:?} is not a component");
870 }
871 wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all())
872 .validate_all(&wasm)
873 .with_context(|| {
874 format!(
875 "compiler produced invalid wasm file {output:?} for component {}",
876 component.name
877 )
878 })?;
879
880 Ok(output)
881 }
882
883 fn runtime_test(
888 &self,
889 case: &Test,
890 runner: &Component,
891 runner_wasm: &Path,
892 test_components: &[(Component, PathBuf)],
893 ) -> Result<()> {
894 let composed = if case.config.wac.is_none() {
900 self.compose_wasm_with_wasm_compose(runner_wasm, test_components)?
901 } else {
902 self.compose_wasm_with_wac(case, runner, runner_wasm, test_components)?
903 };
904
905 let dst = runner_wasm.parent().unwrap();
906 let mut filename = format!(
907 "composed-{}",
908 runner.path.file_name().unwrap().to_str().unwrap(),
909 );
910 for (test, _) in test_components {
911 filename.push_str("-");
912 filename.push_str(test.path.file_name().unwrap().to_str().unwrap());
913 }
914 filename.push_str(".wasm");
915 let composed_wasm = dst.join(filename);
916 write_if_different(&composed_wasm, &composed)?;
917
918 let mut cmd = self.test_runner.command();
919 for component in [runner]
920 .into_iter()
921 .chain(test_components.iter().map(|(c, _)| c))
922 {
923 for flag in Vec::from(component.wasmtime_flags.clone()) {
924 cmd.arg(flag);
925 }
926 }
927 cmd.arg(&composed_wasm);
928 self.run_command(&mut cmd)?;
929 Ok(())
930 }
931
932 fn compose_wasm_with_wasm_compose(
933 &self,
934 runner_wasm: &Path,
935 test_components: &[(Component, PathBuf)],
936 ) -> Result<Vec<u8>> {
937 assert!(test_components.len() > 0);
938 let mut last_bytes = None;
939 let mut path: PathBuf;
940 for (i, (_component, component_path)) in test_components.iter().enumerate() {
941 let main = match last_bytes.take() {
942 Some(bytes) => {
943 path = runner_wasm.with_extension(&format!("composition{i}.wasm"));
944 std::fs::write(&path, &bytes)
945 .with_context(|| format!("failed to write temporary file {path:?}"))?;
946 path.as_path()
947 }
948 None => runner_wasm,
949 };
950
951 let mut config = wasm_compose::config::Config::default();
952 config.definitions = vec![component_path.to_path_buf()];
953 last_bytes = Some(
954 wasm_compose::composer::ComponentComposer::new(main, &config)
955 .compose()
956 .with_context(|| {
957 format!("failed to compose {main:?} with {component_path:?}")
958 })?,
959 );
960 }
961
962 Ok(last_bytes.unwrap())
963 }
964
965 fn compose_wasm_with_wac(
966 &self,
967 case: &Test,
968 runner: &Component,
969 runner_wasm: &Path,
970 test_components: &[(Component, PathBuf)],
971 ) -> Result<Vec<u8>> {
972 let document = match &case.config.wac {
973 Some(path) => {
974 let wac_config = case.path.join(path);
975 fs::read_to_string(&wac_config)
976 .with_context(|| format!("failed to read {wac_config:?}"))?
977 }
978 None => {
981 let mut script = String::from("package example:composition;\n");
982 let mut args = Vec::new();
983 for (component, _path) in test_components {
984 let world = &component.bindgen.world;
985 args.push(format!("...{world}"));
986 script.push_str(&format!("let {world} = new test:{world} {{ ... }};\n"));
987 }
988 args.push("...".to_string());
989 let runner = &runner.bindgen.world;
990 script.push_str(&format!(
991 "let runner = new test:{runner} {{ {} }};\n\
992 export runner...;",
993 args.join(", ")
994 ));
995
996 script
997 }
998 };
999
1000 let components_as_packages = test_components
1003 .iter()
1004 .map(|(component, path)| {
1005 Ok((format!("test:{}", component.bindgen.world), fs::read(path)?))
1006 })
1007 .collect::<Result<Vec<_>>>()?;
1008
1009 let runner_name = format!("test:{}", runner.bindgen.world);
1010 let mut packages = indexmap::IndexMap::new();
1011 packages.insert(
1012 wac_types::BorrowedPackageKey {
1013 name: &runner_name,
1014 version: None,
1015 },
1016 fs::read(runner_wasm)?,
1017 );
1018 for (name, contents) in components_as_packages.iter() {
1019 packages.insert(
1020 wac_types::BorrowedPackageKey {
1021 name,
1022 version: None,
1023 },
1024 contents.clone(),
1025 );
1026 }
1027
1028 let document =
1030 wac_parser::Document::parse(&document).context("failed to parse wac script")?;
1031 document
1032 .resolve(packages)
1033 .context("failed to run `wac` resolve")?
1034 .encode(wac_graph::EncodeOptions {
1035 define_components: true,
1036 validate: false,
1037 processor: None,
1038 })
1039 .context("failed to encode `wac` result")
1040 }
1041
1042 fn run_command(&self, cmd: &mut Command) -> Result<()> {
1045 if self.opts.inherit_stderr {
1046 cmd.stderr(Stdio::inherit());
1047 }
1048 let output = cmd
1049 .output()
1050 .with_context(|| format!("failed to spawn {cmd:?}"))?;
1051 if output.status.success() {
1052 return Ok(());
1053 }
1054
1055 let mut error = format!(
1056 "\
1057command execution failed
1058command: {cmd:?}
1059status: {}",
1060 output.status,
1061 );
1062
1063 if !output.stdout.is_empty() {
1064 error.push_str(&format!(
1065 "\nstdout:\n {}",
1066 String::from_utf8_lossy(&output.stdout).replace("\n", "\n ")
1067 ));
1068 }
1069 if !output.stderr.is_empty() {
1070 error.push_str(&format!(
1071 "\nstderr:\n {}",
1072 String::from_utf8_lossy(&output.stderr).replace("\n", "\n ")
1073 ));
1074 }
1075
1076 bail!("{error}")
1077 }
1078
1079 fn convert_p1_to_component(&self, p1: &Path, compile: &Compile<'_>) -> Result<()> {
1084 let mut resolve = wit_parser::Resolve::default();
1085 let (pkg, _) = resolve
1086 .push_path(&compile.component.bindgen.wit_path)
1087 .context("failed to load WIT")?;
1088 let world = resolve.select_world(&[pkg], Some(&compile.component.bindgen.world))?;
1089 let mut module = fs::read(&p1).context("failed to read wasm file")?;
1090
1091 if !has_component_type_sections(&module) {
1092 let encoded =
1093 wit_component::metadata::encode(&resolve, world, StringEncoding::UTF8, None)?;
1094 let section = wasm_encoder::CustomSection {
1095 name: Cow::Borrowed("component-type"),
1096 data: Cow::Borrowed(&encoded),
1097 };
1098 module.push(section.id());
1099 section.encode(&mut module);
1100 }
1101
1102 let wasi_adapter =
1103 wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
1104
1105 let component = ComponentEncoder::default()
1106 .module(module.as_slice())
1107 .context("failed to load custom sections from input module")?
1108 .validate(true)
1109 .adapter("wasi_snapshot_preview1", wasi_adapter)
1110 .context("failed to load wasip1 adapter")?
1111 .encode()
1112 .context("failed to convert to a component")?;
1113 write_if_different(compile.output, component)?;
1114 Ok(())
1115 }
1116
1117 fn include_language(&self, language: &Language) -> bool {
1119 self.opts
1120 .languages
1121 .iter()
1122 .any(|l| l == language.obj().display())
1123 }
1124
1125 fn render_error(&self, result: StepResult<'_>) -> Result<(), libtest_mimic::Failed> {
1126 let err = match (result.result, result.should_fail) {
1127 (Ok(()), false) | (Err(_), true) => return Ok(()),
1128 (Err(e), false) => e,
1129 (Ok(()), true) => return Err("test should have failed, but passed".into()),
1130 };
1131
1132 let mut s = String::new();
1133 for (k, v) in result.metadata {
1134 s.push_str(&format!(" {k}: {v}\n"));
1135 }
1136 s.push_str(&format!(
1137 " error: {}",
1138 format!("{err:?}").replace("\n", "\n ")
1139 ));
1140 Err(s.into())
1141 }
1142}
1143
1144fn has_component_type_sections(wasm: &[u8]) -> bool {
1145 for payload in wasmparser::Parser::new(0).parse_all(wasm) {
1146 match payload {
1147 Ok(wasmparser::Payload::CustomSection(s)) if s.name().starts_with("component-type") => {
1148 return true;
1149 }
1150 _ => {}
1151 }
1152 }
1153 false
1154}
1155
1156struct StepResult<'a> {
1157 result: Result<()>,
1158 should_fail: bool,
1159 metadata: Vec<(&'a str, String)>,
1160}
1161
1162impl<'a> StepResult<'a> {
1163 fn new(result: Result<()>) -> StepResult<'a> {
1164 StepResult {
1165 result,
1166 should_fail: false,
1167 metadata: Vec::new(),
1168 }
1169 }
1170
1171 fn should_fail(mut self, fail: bool) -> Self {
1172 self.should_fail = fail;
1173 self
1174 }
1175
1176 fn metadata(mut self, name: &'a str, value: impl fmt::Display) -> Self {
1177 self.metadata.push((name, value.to_string()));
1178 self
1179 }
1180}
1181
1182trait LanguageMethods {
1185 fn display(&self) -> &str;
1187
1188 fn comment_prefix_for_test_config(&self) -> Option<&str>;
1194
1195 fn codegen_test_variants(&self) -> &[(&str, &[&str])] {
1204 &[]
1205 }
1206
1207 fn prepare(&self, runner: &mut Runner) -> Result<()>;
1210
1211 fn generate_bindings_prepare(
1213 &self,
1214 _runner: &Runner,
1215 _bindgen: &Bindgen,
1216 _dir: &Path,
1217 ) -> Result<()> {
1218 Ok(())
1219 }
1220
1221 fn generate_bindings(&self, runner: &Runner, bindgen: &Bindgen, dir: &Path) -> Result<()> {
1225 let name = match self.bindgen_name() {
1226 Some(name) => name,
1227 None => return Ok(()),
1228 };
1229 self.generate_bindings_prepare(runner, bindgen, dir)?;
1230 let mut cmd = Command::new(&runner.wit_bindgen);
1231 cmd.arg(name)
1232 .arg(&bindgen.wit_path)
1233 .arg("--world")
1234 .arg(format!("%{}", bindgen.world))
1235 .arg("--out-dir")
1236 .arg(dir);
1237
1238 match bindgen.wit_config.default_bindgen_args {
1239 Some(true) | None => {
1240 for arg in self.default_bindgen_args() {
1241 cmd.arg(arg);
1242 }
1243 }
1244 Some(false) => {}
1245 }
1246
1247 for arg in bindgen.args.iter() {
1248 cmd.arg(arg);
1249 }
1250
1251 runner.run_command(&mut cmd)
1252 }
1253
1254 fn default_bindgen_args(&self) -> &[&str] {
1259 &[]
1260 }
1261
1262 fn default_bindgen_args_for_codegen(&self) -> &[&str] {
1265 &[]
1266 }
1267
1268 fn bindgen_name(&self) -> Option<&str> {
1275 Some(self.display())
1276 }
1277
1278 fn compile(&self, runner: &Runner, compile: &Compile) -> Result<()>;
1280
1281 fn should_fail_verify(&self, name: &str, config: &config::WitConfig, args: &[String]) -> bool;
1284
1285 fn verify(&self, runner: &Runner, verify: &Verify) -> Result<()>;
1288}
1289
1290impl Language {
1291 const ALL: &[Language] = &[
1292 Language::Rust,
1293 Language::C,
1294 Language::Cpp,
1295 Language::Wat,
1296 Language::Csharp,
1297 Language::MoonBit,
1298 Language::Go,
1299 ];
1300
1301 fn obj(&self) -> &dyn LanguageMethods {
1302 match self {
1303 Language::Rust => &rust::Rust,
1304 Language::C => &c::C,
1305 Language::Cpp => &cpp::Cpp,
1306 Language::Wat => &wat::Wat,
1307 Language::Csharp => &csharp::Csharp,
1308 Language::MoonBit => &moonbit::MoonBit,
1309 Language::Go => &go::Go,
1310 Language::Custom(custom) => custom,
1311 }
1312 }
1313}
1314
1315impl fmt::Display for Language {
1316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1317 self.obj().display().fmt(f)
1318 }
1319}
1320
1321impl fmt::Display for Kind {
1322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1323 match self {
1324 Kind::Runner => "runner".fmt(f),
1325 Kind::Test => "test".fmt(f),
1326 }
1327 }
1328}
1329
1330fn write_if_different(path: &Path, contents: impl AsRef<[u8]>) -> Result<bool> {
1333 let contents = contents.as_ref();
1334 if let Ok(prev) = fs::read(path) {
1335 if prev == contents {
1336 return Ok(false);
1337 }
1338 }
1339
1340 if let Some(parent) = path.parent() {
1341 fs::create_dir_all(parent)
1342 .with_context(|| format!("failed to create directory {parent:?}"))?;
1343 }
1344 fs::write(path, contents).with_context(|| format!("failed to write {path:?}"))?;
1345 Ok(true)
1346}
1347
1348impl Component {
1349 fn deserialize_lang_config<T>(&self) -> Result<T>
1355 where
1356 T: Default + serde::de::DeserializeOwned,
1357 {
1358 if self.lang_config.is_none() {
1361 return Ok(T::default());
1362 }
1363
1364 let config = config::parse_test_config::<config::RuntimeTestConfig<T>>(
1369 &self.contents,
1370 self.language
1371 .obj()
1372 .comment_prefix_for_test_config()
1373 .unwrap(),
1374 )?;
1375 Ok(config.lang.unwrap())
1376 }
1377}