1#[cfg(feature = "component-model")]
2use crate::component;
3use crate::core;
4use crate::spectest::*;
5use json_from_wast::{Action, Command, Const, WasmFile, WasmFileType};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::str;
9use std::sync::Arc;
10use std::thread;
11use wasmtime::{error::Context as _, *};
12use wast::lexer::Lexer;
13use wast::parser::{self, ParseBuffer};
14
15pub struct WastContext {
18 current: Option<InstanceKind>,
21 core_linker: Linker<()>,
22 modules: HashMap<String, ModuleKind>,
23 #[cfg(feature = "component-model")]
24 component_linker: component::Linker<()>,
25
26 pub(crate) core_store: Store<()>,
31 pub(crate) async_runtime: Option<tokio::runtime::Runtime>,
32 generate_dwarf: bool,
33 precompile_save: Option<PathBuf>,
34 precompile_load: Option<PathBuf>,
35
36 modules_by_filename: Arc<HashMap<String, Vec<u8>>>,
37 configure_store: Arc<dyn Fn(&mut Store<()>) + Send + Sync>,
38}
39
40enum Outcome<T = Results> {
41 Ok(T),
42 Trap(Error),
43}
44
45impl<T> Outcome<T> {
46 fn map<U>(self, map: impl FnOnce(T) -> U) -> Outcome<U> {
47 match self {
48 Outcome::Ok(t) => Outcome::Ok(map(t)),
49 Outcome::Trap(t) => Outcome::Trap(t),
50 }
51 }
52
53 fn into_result(self) -> Result<T> {
54 match self {
55 Outcome::Ok(t) => Ok(t),
56 Outcome::Trap(t) => Err(t),
57 }
58 }
59}
60
61#[derive(Debug)]
62enum Results {
63 Core(Vec<Val>),
64 #[cfg(feature = "component-model")]
65 Component(Vec<component::Val>),
66}
67
68#[derive(Clone)]
69enum ModuleKind {
70 Core(Module),
71 #[cfg(feature = "component-model")]
72 Component(component::Component),
73}
74
75enum InstanceKind {
76 Core(Instance),
77 #[cfg(feature = "component-model")]
78 Component(Store<()>, component::Instance),
79}
80
81enum Export<'a> {
82 Core(Extern),
83 #[cfg(feature = "component-model")]
84 Component(&'a mut Store<()>, component::Func),
85
86 _Unused(std::convert::Infallible, &'a ()),
89}
90
91#[derive(Debug, Copy, Clone, PartialEq)]
95#[expect(missing_docs, reason = "self-describing variants")]
96pub enum Async {
97 Yes,
98 No,
99}
100
101impl WastContext {
102 pub fn new(
110 engine: &Engine,
111 async_: Async,
112 configure: impl Fn(&mut Store<()>) + Send + Sync + 'static,
113 ) -> Self {
114 let mut core_linker = Linker::new(engine);
118 core_linker.allow_shadowing(true);
119 Self {
120 current: None,
121 core_linker,
122 #[cfg(feature = "component-model")]
123 component_linker: {
124 let mut linker = component::Linker::new(engine);
125 linker.allow_shadowing(true);
126 linker
127 },
128 core_store: {
129 let mut store = Store::new(engine, ());
130 configure(&mut store);
131 store
132 },
133 modules: Default::default(),
134 async_runtime: if async_ == Async::Yes {
135 Some(
136 tokio::runtime::Builder::new_current_thread()
137 .build()
138 .unwrap(),
139 )
140 } else {
141 None
142 },
143 generate_dwarf: true,
144 precompile_save: None,
145 precompile_load: None,
146 modules_by_filename: Arc::default(),
147 configure_store: Arc::new(configure),
148 }
149 }
150
151 fn engine(&self) -> &Engine {
152 self.core_linker.engine()
153 }
154
155 pub fn precompile_save(&mut self, path: impl AsRef<Path>) -> &mut Self {
158 self.precompile_save = Some(path.as_ref().into());
159 self
160 }
161
162 pub fn precompile_load(&mut self, path: impl AsRef<Path>) -> &mut Self {
165 self.precompile_load = Some(path.as_ref().into());
166 self
167 }
168
169 fn get_export(&mut self, module: Option<&str>, name: &str) -> Result<Export<'_>> {
170 if let Some(module) = module {
171 return Ok(Export::Core(
172 self.core_linker
173 .get(&mut self.core_store, module, name)
174 .ok_or_else(|| format_err!("no item named `{module}::{name}` found"))?,
175 ));
176 }
177
178 let cur = self
179 .current
180 .as_mut()
181 .ok_or_else(|| format_err!("no previous instance found"))?;
182 Ok(match cur {
183 InstanceKind::Core(i) => Export::Core(
184 i.get_export(&mut self.core_store, name)
185 .ok_or_else(|| format_err!("no item named `{name}` found"))?,
186 ),
187 #[cfg(feature = "component-model")]
188 InstanceKind::Component(store, i) => {
189 let export = i
190 .get_func(&mut *store, name)
191 .ok_or_else(|| format_err!("no func named `{name}` found"))?;
192 Export::Component(store, export)
193 }
194 })
195 }
196
197 fn instantiate_module(&mut self, module: &Module) -> Result<Outcome<Instance>> {
198 let instance = match &self.async_runtime {
199 Some(rt) => rt.block_on(
200 self.core_linker
201 .instantiate_async(&mut self.core_store, &module),
202 ),
203 None => self.core_linker.instantiate(&mut self.core_store, &module),
204 };
205 Ok(match instance {
206 Ok(i) => Outcome::Ok(i),
207 Err(e) => Outcome::Trap(e),
208 })
209 }
210
211 #[cfg(feature = "component-model")]
212 fn instantiate_component(
213 &mut self,
214 component: &component::Component,
215 ) -> Result<Outcome<(component::Component, Store<()>, component::Instance)>> {
216 let mut store = Store::new(self.engine(), ());
217 (self.configure_store)(&mut store);
218 let instance = match &self.async_runtime {
219 Some(rt) => rt.block_on(
220 self.component_linker
221 .instantiate_async(&mut store, &component),
222 ),
223 None => self.component_linker.instantiate(&mut store, &component),
224 };
225 Ok(match instance {
226 Ok(i) => Outcome::Ok((component.clone(), store, i)),
227 Err(e) => Outcome::Trap(e),
228 })
229 }
230
231 pub fn register_spectest(&mut self, config: &SpectestConfig) -> Result<()> {
233 link_spectest(&mut self.core_linker, &mut self.core_store, config)?;
234 #[cfg(feature = "component-model")]
235 link_component_spectest(&mut self.component_linker)?;
236 Ok(())
237 }
238
239 fn perform_action(&mut self, action: &Action<'_>) -> Result<Outcome> {
241 struct ReplaceRuntime<'a> {
246 ctx: &'a mut WastContext,
247 rt: Option<tokio::runtime::Runtime>,
248 }
249 impl Drop for ReplaceRuntime<'_> {
250 fn drop(&mut self) {
251 self.ctx.async_runtime = self.rt.take();
252 }
253 }
254 let replace = ReplaceRuntime {
255 rt: self.async_runtime.take(),
256 ctx: self,
257 };
258 let me = &mut *replace.ctx;
259 match action {
260 Action::Invoke {
261 module,
262 field,
263 args,
264 } => match me.get_export(module.as_deref(), field)? {
265 Export::Core(export) => {
266 drop(replace);
267 let func = export
268 .into_func()
269 .ok_or_else(|| format_err!("no function named `{field}`"))?;
270 let values = args
271 .iter()
272 .map(|v| match v {
273 Const::Core(v) => core::val(self, v),
274 _ => bail!("expected core function, found other other argument {v:?}"),
275 })
276 .collect::<Result<Vec<_>>>()?;
277
278 let mut results =
279 vec![Val::null_func_ref(); func.ty(&self.core_store).results().len()];
280 let result = match &self.async_runtime {
281 Some(rt) => rt.block_on(func.call_async(
282 &mut self.core_store,
283 &values,
284 &mut results,
285 )),
286 None => func.call(&mut self.core_store, &values, &mut results),
287 };
288
289 Ok(match result {
290 Ok(()) => Outcome::Ok(Results::Core(results)),
291 Err(e) => Outcome::Trap(e),
292 })
293 }
294 #[cfg(feature = "component-model")]
295 Export::Component(store, func) => {
296 let values = args
297 .iter()
298 .map(|v| match v {
299 Const::Component(v) => component::val(v),
300 _ => bail!("expected component function, found other argument {v:?}"),
301 })
302 .collect::<Result<Vec<_>>>()?;
303
304 let mut results =
305 vec![component::Val::Bool(false); func.ty(&store).results().len()];
306 let result = match &replace.rt {
307 Some(rt) => {
308 rt.block_on(func.call_async(&mut *store, &values, &mut results))
309 }
310 None => func.call(&mut *store, &values, &mut results),
311 };
312 Ok(match result {
313 Ok(()) => Outcome::Ok(Results::Component(results)),
314 Err(e) => Outcome::Trap(e),
315 })
316 }
317 },
318 Action::Get { module, field, .. } => me.get(module.as_deref(), field),
319 }
320 }
321
322 fn module(&mut self, name: Option<&str>, module: &ModuleKind) -> Result<()> {
325 match module {
326 ModuleKind::Core(module) => {
327 let instance = match self.instantiate_module(&module)? {
328 Outcome::Ok(i) => i,
329 Outcome::Trap(e) => return Err(e).context("instantiation failed"),
330 };
331 if let Some(name) = name {
332 self.core_linker
333 .instance(&mut self.core_store, name, instance)?;
334 }
335 self.current = Some(InstanceKind::Core(instance));
336 }
337 #[cfg(feature = "component-model")]
338 ModuleKind::Component(module) => {
339 let (component, mut store, instance) = match self.instantiate_component(&module)? {
340 Outcome::Ok(i) => i,
341 Outcome::Trap(e) => return Err(e).context("instantiation failed"),
342 };
343 if let Some(name) = name {
344 let ty = component.component_type();
345 let engine = self.engine().clone();
346 let mut linker = self.component_linker.instance(name)?;
347 for (name, item) in ty.exports(&engine) {
348 match item {
349 component::types::ComponentItem::Module(_) => {
350 let module = instance.get_module(&mut store, name).unwrap();
351 linker.module(name, &module)?;
352 }
353 component::types::ComponentItem::Resource(_) => {
354 let resource = instance.get_resource(&mut store, name).unwrap();
355 linker.resource(name, resource, |_, _| Ok(()))?;
356 }
357 _ => {}
364 }
365 }
366 }
367 self.current = Some(InstanceKind::Component(store, instance));
368 }
369 }
370 Ok(())
371 }
372
373 fn module_definition(&mut self, file: &WasmFile) -> Result<ModuleKind> {
378 let name = match file.module_type {
379 WasmFileType::Text => file
380 .binary_filename
381 .as_ref()
382 .ok_or_else(|| format_err!("cannot compile module that isn't a valid binary"))?,
383 WasmFileType::Binary => &file.filename,
384 };
385
386 match &self.precompile_load {
387 Some(path) => {
388 let cwasm = path.join(&name[..]).with_extension("cwasm");
389 match Engine::detect_precompiled_file(&cwasm)
390 .with_context(|| format!("failed to read {cwasm:?}"))?
391 {
392 Some(Precompiled::Module) => {
393 let module = unsafe { Module::deserialize_file(self.engine(), &cwasm)? };
394 Ok(ModuleKind::Core(module))
395 }
396 #[cfg(feature = "component-model")]
397 Some(Precompiled::Component) => {
398 let component = unsafe {
399 component::Component::deserialize_file(self.engine(), &cwasm)?
400 };
401 Ok(ModuleKind::Component(component))
402 }
403 #[cfg(not(feature = "component-model"))]
404 Some(Precompiled::Component) => {
405 bail!("support for components disabled at compile time")
406 }
407 None => bail!("expected a cwasm file"),
408 }
409 }
410 None => {
411 let bytes = &self.modules_by_filename[&name[..]];
412
413 if wasmparser::Parser::is_core_wasm(&bytes) {
414 let module = Module::new(self.engine(), &bytes)?;
415 Ok(ModuleKind::Core(module))
416 } else {
417 #[cfg(feature = "component-model")]
418 {
419 let component = component::Component::new(self.engine(), &bytes)?;
420 Ok(ModuleKind::Component(component))
421 }
422 #[cfg(not(feature = "component-model"))]
423 bail!("component-model support not enabled");
424 }
425 }
426 }
427 }
428
429 fn register(&mut self, name: Option<&str>, as_name: &str) -> Result<()> {
431 match name {
432 Some(name) => self.core_linker.alias_module(name, as_name),
433 None => {
434 let current = self
435 .current
436 .as_ref()
437 .ok_or(format_err!("no previous instance"))?;
438 match current {
439 InstanceKind::Core(current) => {
440 self.core_linker
441 .instance(&mut self.core_store, as_name, *current)?;
442 }
443 #[cfg(feature = "component-model")]
444 InstanceKind::Component(..) => {
445 bail!("register not implemented for components");
446 }
447 }
448 Ok(())
449 }
450 }
451 }
452
453 fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result<Outcome> {
455 let global = match self.get_export(instance_name, field)? {
456 Export::Core(e) => e
457 .into_global()
458 .ok_or_else(|| format_err!("no global named `{field}`"))?,
459 #[cfg(feature = "component-model")]
460 Export::Component(..) => bail!("no global named `{field}`"),
461 };
462 Ok(Outcome::Ok(Results::Core(vec![
463 global.get(&mut self.core_store),
464 ])))
465 }
466
467 fn assert_return(&mut self, result: Outcome, results: &[Const]) -> Result<()> {
468 match result.into_result()? {
469 Results::Core(values) => {
470 if values.len() != results.len() {
471 bail!("expected {} results found {}", results.len(), values.len());
472 }
473 for (i, (v, e)) in values.iter().zip(results).enumerate() {
474 let e = match e {
475 Const::Core(core) => core,
476 _ => bail!("expected core value found other value {e:?}"),
477 };
478 core::match_val(&mut self.core_store, v, e)
479 .with_context(|| format!("result {i} didn't match"))?;
480 }
481 }
482 #[cfg(feature = "component-model")]
483 Results::Component(values) => {
484 if values.len() != results.len() {
485 bail!("expected {} results found {}", results.len(), values.len());
486 }
487 for (i, (v, e)) in values.iter().zip(results).enumerate() {
488 let e = match e {
489 Const::Component(val) => val,
490 _ => bail!("expected component value found other value {e:?}"),
491 };
492 component::match_val(e, v)
493 .with_context(|| format!("result {i} didn't match"))?;
494 }
495 }
496 }
497 Ok(())
498 }
499
500 fn assert_trap(&self, result: Outcome, expected: &str) -> Result<()> {
501 let trap = match result {
502 Outcome::Ok(values) => bail!("expected trap, got {values:?}"),
503 Outcome::Trap(t) => t,
504 };
505 let actual = format!("{trap:?}");
506 if actual.contains(expected)
507 || (expected.contains("uninitialized element 2") && actual.contains("uninitialized element"))
511 || (expected.contains("null function") && (actual.contains("uninitialized element") || actual.contains("null reference")))
513 || (expected.contains("null") && expected.contains("reference") && actual.contains("null reference"))
515 {
516 return Ok(());
517 }
518 bail!("expected '{expected}', got '{actual}'")
519 }
520
521 fn assert_exception(&mut self, result: Outcome) -> Result<()> {
522 match result {
523 Outcome::Ok(values) => bail!("expected exception, got {values:?}"),
524 Outcome::Trap(err) if err.is::<ThrownException>() => {
525 let _ = self
527 .core_store
528 .take_pending_exception()
529 .expect("there should be a pending exception on the store");
530 Ok(())
531 }
532 Outcome::Trap(err) => bail!("expected exception, got {err:?}"),
533 }
534 }
535
536 pub fn run_wast(&mut self, filename: &str, wast: &[u8]) -> Result<()> {
538 let wast = str::from_utf8(wast)?;
539
540 let adjust_wast = |mut err: wast::Error| {
541 err.set_path(filename.as_ref());
542 err.set_text(wast);
543 err
544 };
545
546 let mut lexer = Lexer::new(wast);
547 lexer.allow_confusing_unicode(filename.ends_with("names.wast"));
548 let mut buf = ParseBuffer::new_with_lexer(lexer).map_err(adjust_wast)?;
549 buf.track_instr_spans(self.generate_dwarf);
550 let ast = parser::parse::<wast::Wast>(&buf).map_err(adjust_wast)?;
551
552 let mut ast = json_from_wast::Opts::default()
553 .dwarf(self.generate_dwarf)
554 .convert(filename, wast, ast)
555 .to_wasmtime_result()?;
556
557 if !self.modules_by_filename.is_empty() {
560 self.modules_by_filename = Arc::default();
561 }
562 let modules_by_filename = Arc::get_mut(&mut self.modules_by_filename).unwrap();
563 for (name, bytes) in ast.wasms.drain(..) {
564 let prev = modules_by_filename.insert(name, bytes);
565 assert!(prev.is_none());
566 }
567
568 match &self.precompile_save {
569 Some(path) => {
570 let json_path = path
571 .join(Path::new(filename).file_name().unwrap())
572 .with_extension("json");
573 let json = serde_json::to_string(&ast)?;
574 std::fs::write(&json_path, json)
575 .with_context(|| format!("failed to write {json_path:?}"))?;
576 for (name, bytes) in self.modules_by_filename.iter() {
577 let cwasm_path = path.join(name).with_extension("cwasm");
578 let cwasm = if wasmparser::Parser::is_core_wasm(&bytes) {
579 self.engine().precompile_module(bytes)
580 } else {
581 #[cfg(feature = "component-model")]
582 {
583 self.engine().precompile_component(bytes)
584 }
585 #[cfg(not(feature = "component-model"))]
586 bail!("component-model support not enabled");
587 };
588 if let Ok(cwasm) = cwasm {
589 std::fs::write(&cwasm_path, cwasm)
590 .with_context(|| format!("failed to write {cwasm_path:?}"))?;
591 }
592 }
593 Ok(())
594 }
595 None => self.run_directives(ast.commands, filename),
596 }
597 }
598
599 fn run_directives(&mut self, directives: Vec<Command<'_>>, filename: &str) -> Result<()> {
600 thread::scope(|scope| {
601 let mut threads = HashMap::new();
602 for directive in directives {
603 let line = directive.line();
604 log::debug!("running directive on {filename}:{line}");
605 self.run_directive(directive, filename, &scope, &mut threads)
606 .with_context(|| format!("failed directive on {filename}:{line}"))?;
607 }
608 Ok(())
609 })
610 }
611
612 fn run_directive<'a>(
613 &mut self,
614 directive: Command<'a>,
615 filename: &'a str,
616 scope: &'a thread::Scope<'a, '_>,
618 threads: &mut HashMap<String, thread::ScopedJoinHandle<'a, Result<()>>>,
619 ) -> Result<()> {
620 use Command::*;
621
622 match directive {
623 Module {
624 name,
625 file,
626 line: _,
627 } => {
628 let module = self.module_definition(&file)?;
629 self.module(name.as_deref(), &module)?;
630 }
631 ModuleDefinition {
632 name,
633 file,
634 line: _,
635 } => {
636 let module = self.module_definition(&file)?;
637 if let Some(name) = name {
638 self.modules.insert(name.to_string(), module);
639 }
640 }
641 ModuleInstance {
642 instance,
643 module,
644 line: _,
645 } => {
646 let module = module
647 .as_deref()
648 .and_then(|n| self.modules.get(n))
649 .cloned()
650 .ok_or_else(|| format_err!("no module named {module:?}"))?;
651 self.module(instance.as_deref(), &module)?;
652 }
653 Register { line: _, name, as_ } => {
654 self.register(name.as_deref(), &as_)?;
655 }
656 Action { action, line: _ } => {
657 self.perform_action(&action)?;
658 }
659 AssertReturn {
660 action,
661 expected,
662 line: _,
663 } => {
664 let result = self.perform_action(&action)?;
665 self.assert_return(result, &expected)?;
666 }
667 AssertTrap {
668 action,
669 text,
670 line: _,
671 } => {
672 let result = self.perform_action(&action)?;
673 self.assert_trap(result, &text)?;
674 }
675 AssertUninstantiable {
676 file,
677 text,
678 line: _,
679 } => {
680 let result = match self.module_definition(&file)? {
681 ModuleKind::Core(module) => self
682 .instantiate_module(&module)?
683 .map(|_| Results::Core(Vec::new())),
684 #[cfg(feature = "component-model")]
685 ModuleKind::Component(component) => self
686 .instantiate_component(&component)?
687 .map(|_| Results::Component(Vec::new())),
688 };
689 self.assert_trap(result, &text)?;
690 }
691 AssertExhaustion {
692 action,
693 text,
694 line: _,
695 } => {
696 let result = self.perform_action(&action)?;
697 self.assert_trap(result, &text)?;
698 }
699 AssertInvalid {
700 file,
701 text,
702 line: _,
703 } => {
704 let err = match self.module_definition(&file) {
705 Ok(_) => bail!("expected module to fail to build"),
706 Err(e) => e,
707 };
708 let error_message = format!("{err:?}");
709 if !is_matching_assert_invalid_error_message(filename, &text, &error_message) {
710 bail!("assert_invalid: expected \"{text}\", got \"{error_message}\"",)
711 }
712 }
713 AssertMalformed {
714 file,
715 text: _,
716 line: _,
717 } => {
718 if let Ok(_) = self.module_definition(&file) {
719 bail!("expected malformed module to fail to instantiate");
720 }
721 }
722 AssertUnlinkable {
723 file,
724 text,
725 line: _,
726 } => {
727 let module = self.module_definition(&file)?;
728 let err = match self.module(None, &module) {
729 Ok(_) => bail!("expected module to fail to link"),
730 Err(e) => e,
731 };
732 let error_message = format!("{err:?}");
733 if !is_matching_assert_invalid_error_message(filename, &text, &error_message) {
734 bail!("assert_unlinkable: expected {text}, got {error_message}",)
735 }
736 }
737 AssertException { line: _, action } => {
738 let result = self.perform_action(&action)?;
739 self.assert_exception(result)?;
740 }
741
742 Thread {
743 name,
744 shared_module,
745 commands,
746 line: _,
747 } => {
748 let mut core_linker = Linker::new(self.engine());
749 if let Some(id) = shared_module {
750 let items = self
751 .core_linker
752 .iter(&mut self.core_store)
753 .filter(|(module, _, _)| *module == &id[..])
754 .collect::<Vec<_>>();
755 for (module, name, item) in items {
756 core_linker.define(&mut self.core_store, module, name, item)?;
757 }
758 }
759 let mut child_cx = WastContext {
760 current: None,
761 core_linker,
762 #[cfg(feature = "component-model")]
763 component_linker: component::Linker::new(self.engine()),
764 core_store: {
765 let mut store = Store::new(self.engine(), ());
766 (self.configure_store)(&mut store);
767 store
768 },
769 modules: self.modules.clone(),
770 async_runtime: self.async_runtime.as_ref().map(|_| {
771 tokio::runtime::Builder::new_current_thread()
772 .build()
773 .unwrap()
774 }),
775 generate_dwarf: self.generate_dwarf,
776 modules_by_filename: self.modules_by_filename.clone(),
777 precompile_load: self.precompile_load.clone(),
778 precompile_save: self.precompile_save.clone(),
779 configure_store: self.configure_store.clone(),
780 };
781 let child = scope.spawn(move || child_cx.run_directives(commands, filename));
782 threads.insert(name.to_string(), child);
783 }
784 Wait { thread, .. } => {
785 threads
786 .remove(&thread[..])
787 .ok_or_else(|| format_err!("no thread named `{thread}`"))?
788 .join()
789 .unwrap()?;
790 }
791
792 AssertSuspension { .. } => {
793 bail!("unimplemented wast directive");
794 }
795 }
796
797 Ok(())
798 }
799
800 pub fn run_file(&mut self, path: &Path) -> Result<()> {
802 match &self.precompile_load {
803 Some(precompile) => {
804 let file = precompile
805 .join(path.file_name().unwrap())
806 .with_extension("json");
807 let json = std::fs::read_to_string(&file)
808 .with_context(|| format!("failed to read {file:?}"))?;
809 let wast = serde_json::from_str::<json_from_wast::Wast<'_>>(&json)?;
810 self.run_directives(wast.commands, &wast.source_filename)
811 }
812 None => {
813 let bytes = std::fs::read(path)
814 .with_context(|| format!("failed to read `{}`", path.display()))?;
815 self.run_wast(path.to_str().unwrap(), &bytes)
816 }
817 }
818 }
819
820 pub fn generate_dwarf(&mut self, enable: bool) -> &mut Self {
823 self.generate_dwarf = enable;
824 self
825 }
826}
827
828fn is_matching_assert_invalid_error_message(test: &str, expected: &str, actual: &str) -> bool {
829 if actual.contains(expected) {
830 return true;
831 }
832
833 if test.contains("spec_testsuite") {
841 return true;
842 }
843
844 false
847}