1use crate::prelude::*;
25use crate::{Engine, ModuleVersionStrategy, Precompiled};
26use core::fmt;
27use core::str::FromStr;
28use object::endian::Endianness;
29#[cfg(any(feature = "cranelift", feature = "winch"))]
30use object::write::{Object, StandardSegment};
31use object::{
32 FileFlags, Object as _,
33 elf::FileHeader64,
34 read::elf::{ElfFile64, FileHeader, SectionHeader},
35};
36use serde_derive::{Deserialize, Serialize};
37use wasmtime_environ::obj;
38use wasmtime_environ::{FlagValue, ObjectKind, Tunables};
39
40const VERSION: u8 = 0;
41
42pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> Result<()> {
51 let header = FileHeader64::<Endianness>::parse(mmap)
65 .map_err(obj::ObjectCrateErrorWrapper)
66 .context("failed to parse precompiled artifact as an ELF")?;
67 let endian = header
68 .endian()
69 .context("failed to parse header endianness")?;
70
71 let expected_e_flags = match expected {
72 ObjectKind::Module => obj::EF_WASMTIME_MODULE,
73 ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
74 };
75 ensure!(
76 (header.e_flags(endian) & expected_e_flags) == expected_e_flags,
77 "incompatible object file format"
78 );
79
80 let section_headers = header
81 .section_headers(endian, mmap)
82 .context("failed to parse section headers")?;
83 let strings = header
84 .section_strings(endian, mmap, section_headers)
85 .context("failed to parse strings table")?;
86 let sections = header
87 .sections(endian, mmap)
88 .context("failed to parse sections table")?;
89
90 let mut section_header = None;
91 for s in sections.iter() {
92 let name = s.name(endian, strings)?;
93 if name == obj::ELF_WASM_ENGINE.as_bytes() {
94 section_header = Some(s);
95 }
96 }
97 let Some(section_header) = section_header else {
98 bail!("failed to find section `{}`", obj::ELF_WASM_ENGINE)
99 };
100 let data = section_header
101 .data(endian, mmap)
102 .map_err(obj::ObjectCrateErrorWrapper)?;
103 let (first, data) = data
104 .split_first()
105 .ok_or_else(|| format_err!("invalid engine section"))?;
106 if *first != VERSION {
107 bail!("mismatched version in engine section");
108 }
109 let (len, data) = data
110 .split_first()
111 .ok_or_else(|| format_err!("invalid engine section"))?;
112 let len = usize::from(*len);
113 let (version, data) = if data.len() < len + 1 {
114 bail!("engine section too small")
115 } else {
116 data.split_at(len)
117 };
118
119 match &engine.config().module_version {
120 ModuleVersionStrategy::None => { }
121 _ => {
122 let version = core::str::from_utf8(&version)?;
123 if version != engine.config().module_version.as_str() {
124 bail!("Module was compiled with incompatible version '{version}'");
125 }
126 }
127 }
128 postcard::from_bytes::<Metadata<'_>>(data)?.check_compatible(engine)
129}
130
131#[cfg(any(feature = "cranelift", feature = "winch"))]
132pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Metadata<'_>) {
133 let section = obj.add_section(
134 obj.segment_name(StandardSegment::Data).to_vec(),
135 obj::ELF_WASM_ENGINE.as_bytes().to_vec(),
136 object::SectionKind::ReadOnlyData,
137 );
138 let mut data = Vec::new();
139 data.push(VERSION);
140 let version = engine.config().module_version.as_str();
141 assert!(
143 version.len() < 256,
144 "package version must be less than 256 bytes"
145 );
146 data.push(version.len() as u8);
147 data.extend_from_slice(version.as_bytes());
148 data.extend(postcard::to_allocvec(metadata).unwrap());
149 obj.set_section_data(section, data, 1);
150}
151
152fn detect_precompiled<'data, R: object::ReadRef<'data>>(
153 obj: ElfFile64<'data, Endianness, R>,
154) -> Option<Precompiled> {
155 match obj.flags() {
156 FileFlags::Elf {
157 os_abi: obj::ELFOSABI_WASMTIME,
158 abi_version: 0,
159 e_flags,
160 } if e_flags & obj::EF_WASMTIME_MODULE != 0 => Some(Precompiled::Module),
161 FileFlags::Elf {
162 os_abi: obj::ELFOSABI_WASMTIME,
163 abi_version: 0,
164 e_flags,
165 } if e_flags & obj::EF_WASMTIME_COMPONENT != 0 => Some(Precompiled::Component),
166 _ => None,
167 }
168}
169
170pub fn detect_precompiled_bytes(bytes: &[u8]) -> Option<Precompiled> {
171 detect_precompiled(ElfFile64::parse(bytes).ok()?)
172}
173
174#[cfg(feature = "std")]
175pub fn detect_precompiled_file(path: impl AsRef<std::path::Path>) -> Result<Option<Precompiled>> {
176 let read_cache = object::ReadCache::new(std::fs::File::open(path)?);
177 let obj = ElfFile64::parse(&read_cache)?;
178 Ok(detect_precompiled(obj))
179}
180
181#[derive(Serialize, Deserialize)]
182pub struct Metadata<'a> {
183 target: String,
184 #[serde(borrow)]
185 shared_flags: Vec<(&'a str, FlagValue<'a>)>,
186 #[serde(borrow)]
187 isa_flags: Vec<(&'a str, FlagValue<'a>)>,
188 tunables: Tunables,
189 features: u64,
190}
191
192impl Metadata<'_> {
193 #[cfg(any(feature = "cranelift", feature = "winch"))]
194 pub fn new(engine: &Engine) -> Result<Metadata<'static>> {
195 let compiler = engine.try_compiler()?;
196 Ok(Metadata {
197 target: compiler.triple().to_string(),
198 shared_flags: compiler.flags(),
199 isa_flags: compiler.isa_flags(),
200 tunables: engine.tunables().clone(),
201 features: engine.features().bits(),
202 })
203 }
204
205 fn check_compatible(mut self, engine: &Engine) -> Result<()> {
206 self.check_triple(engine)?;
207 self.check_shared_flags(engine)?;
208 self.check_isa_flags(engine)?;
209 self.check_tunables(&engine.tunables())?;
210 self.check_features(&engine.features())?;
211 Ok(())
212 }
213
214 fn check_triple(&self, engine: &Engine) -> Result<()> {
215 let engine_target = engine.target();
216 let module_target =
217 target_lexicon::Triple::from_str(&self.target).map_err(|e| format_err!(e))?;
218
219 if module_target.architecture != engine_target.architecture {
220 bail!(
221 "Module was compiled for architecture '{}'",
222 module_target.architecture
223 );
224 }
225
226 if module_target.operating_system != engine_target.operating_system {
227 bail!(
228 "Module was compiled for operating system '{}'",
229 module_target.operating_system
230 );
231 }
232
233 Ok(())
234 }
235
236 fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
237 for (name, val) in self.shared_flags.iter() {
238 engine
239 .check_compatible_with_shared_flag(name, val)
240 .map_err(|s| crate::Error::msg(s))
241 .context("compilation settings of module incompatible with native host")?;
242 }
243 Ok(())
244 }
245
246 fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
247 for (name, val) in self.isa_flags.iter() {
248 engine
249 .check_compatible_with_isa_flag(name, val)
250 .map_err(|s| crate::Error::msg(s))
251 .context("compilation settings of module incompatible with native host")?;
252 }
253 Ok(())
254 }
255
256 fn check_int<T: Eq + fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
257 if found == expected {
258 return Ok(());
259 }
260
261 bail!(
262 "Module was compiled with a {feature} of '{found}' but '{expected}' is expected for the host"
263 );
264 }
265
266 fn check_bool(found: bool, expected: bool, feature: impl fmt::Display) -> Result<()> {
267 if found == expected {
268 return Ok(());
269 }
270
271 bail!(
272 "Module was compiled {} {} but it {} enabled for the host",
273 if found { "with" } else { "without" },
274 feature,
275 if expected { "is" } else { "is not" }
276 );
277 }
278
279 fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
280 let Tunables {
281 collector,
282 memory_reservation,
283 memory_guard_size,
284 debug_native,
285 debug_guest,
286 parse_wasm_debuginfo,
287 consume_fuel,
288 epoch_interruption,
289 memory_may_move,
290 guard_before_linear_memory,
291 table_lazy_init,
292 relaxed_simd_deterministic,
293 winch_callable,
294 signals_based_traps,
295 memory_init_cow,
296 inlining,
297 inlining_intra_module,
298 inlining_small_callee_size,
299 inlining_sum_size_threshold,
300 concurrency_support,
301
302 memory_reservation_for_growth: _,
304
305 generate_address_map: _,
310
311 debug_adapter_modules: _,
313 } = self.tunables;
314
315 Self::check_collector(collector, other.collector)?;
316 Self::check_int(
317 memory_reservation,
318 other.memory_reservation,
319 "memory reservation",
320 )?;
321 Self::check_int(
322 memory_guard_size,
323 other.memory_guard_size,
324 "memory guard size",
325 )?;
326 Self::check_bool(
327 debug_native,
328 other.debug_native,
329 "native debug information support",
330 )?;
331 Self::check_bool(debug_guest, other.debug_guest, "guest debug")?;
332 Self::check_bool(
333 parse_wasm_debuginfo,
334 other.parse_wasm_debuginfo,
335 "WebAssembly backtrace support",
336 )?;
337 Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
338 Self::check_bool(
339 epoch_interruption,
340 other.epoch_interruption,
341 "epoch interruption",
342 )?;
343 Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
344 Self::check_bool(
345 guard_before_linear_memory,
346 other.guard_before_linear_memory,
347 "guard before linear memory",
348 )?;
349 Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
350 Self::check_bool(
351 relaxed_simd_deterministic,
352 other.relaxed_simd_deterministic,
353 "relaxed simd deterministic semantics",
354 )?;
355 Self::check_bool(
356 winch_callable,
357 other.winch_callable,
358 "Winch calling convention",
359 )?;
360 Self::check_bool(
361 signals_based_traps,
362 other.signals_based_traps,
363 "Signals-based traps",
364 )?;
365 Self::check_bool(
366 memory_init_cow,
367 other.memory_init_cow,
368 "memory initialization with CoW",
369 )?;
370 Self::check_bool(inlining, other.inlining, "function inlining")?;
371 Self::check_int(
372 inlining_small_callee_size,
373 other.inlining_small_callee_size,
374 "function inlining small-callee size",
375 )?;
376 Self::check_int(
377 inlining_sum_size_threshold,
378 other.inlining_sum_size_threshold,
379 "function inlining sum-size threshold",
380 )?;
381 Self::check_bool(
382 concurrency_support,
383 other.concurrency_support,
384 "concurrency support",
385 )?;
386 Self::check_intra_module_inlining(inlining_intra_module, other.inlining_intra_module)?;
387
388 Ok(())
389 }
390
391 fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
392 let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
393 let missing_features = (*other & module_features) ^ module_features;
394 for (name, _) in missing_features.iter_names() {
395 let name = name.to_ascii_lowercase();
396 bail!(
397 "Module was compiled with support for WebAssembly feature \
398 `{name}` but it is not enabled for the host",
399 );
400 }
401 Ok(())
402 }
403
404 fn check_collector(
405 module: Option<wasmtime_environ::Collector>,
406 host: Option<wasmtime_environ::Collector>,
407 ) -> Result<()> {
408 match (module, host) {
409 (None, _) => Ok(()),
412 (Some(module), Some(host)) if module == host => Ok(()),
413
414 (Some(_), None) => {
415 bail!("module was compiled with GC however GC is disabled in the host")
416 }
417
418 (Some(module), Some(host)) => {
419 bail!(
420 "module was compiled for the {module} collector but \
421 the host is configured to use the {host} collector",
422 )
423 }
424 }
425 }
426
427 fn check_intra_module_inlining(
428 module: wasmtime_environ::IntraModuleInlining,
429 host: wasmtime_environ::IntraModuleInlining,
430 ) -> Result<()> {
431 if module == host {
432 return Ok(());
433 }
434
435 let desc = |cfg| match cfg {
436 wasmtime_environ::IntraModuleInlining::No => "without intra-module inlining",
437 wasmtime_environ::IntraModuleInlining::Yes => "with intra-module inlining",
438 wasmtime_environ::IntraModuleInlining::WhenUsingGc => {
439 "with intra-module inlining only when using GC"
440 }
441 };
442
443 let module = desc(module);
444 let host = desc(host);
445
446 bail!("module was compiled {module} however the host is configured {host}")
447 }
448}
449
450#[cfg(test)]
451mod test {
452 use super::*;
453 use crate::{Cache, Config, Module, OptLevel};
454 use std::{
455 collections::hash_map::DefaultHasher,
456 hash::{Hash, Hasher},
457 };
458 use tempfile::TempDir;
459
460 #[test]
461 fn test_architecture_mismatch() -> Result<()> {
462 let engine = Engine::default();
463 let mut metadata = Metadata::new(&engine)?;
464 metadata.target = "unknown-generic-linux".to_string();
465
466 match metadata.check_compatible(&engine) {
467 Ok(_) => unreachable!(),
468 Err(e) => assert_eq!(
469 e.to_string(),
470 "Module was compiled for architecture 'unknown'",
471 ),
472 }
473
474 Ok(())
475 }
476
477 #[test]
479 #[cfg(all(target_arch = "x86_64", not(miri)))]
480 fn test_os_mismatch() -> Result<()> {
481 let engine = Engine::default();
482 let mut metadata = Metadata::new(&engine)?;
483
484 metadata.target = format!(
485 "{}-generic-unknown",
486 target_lexicon::Triple::host().architecture
487 );
488
489 match metadata.check_compatible(&engine) {
490 Ok(_) => unreachable!(),
491 Err(e) => assert_eq!(
492 e.to_string(),
493 "Module was compiled for operating system 'unknown'",
494 ),
495 }
496
497 Ok(())
498 }
499
500 fn assert_contains(error: &Error, msg: &str) {
501 let msg = msg.trim();
502 if error.chain().any(|e| e.to_string().contains(msg)) {
503 return;
504 }
505
506 panic!("failed to find:\n\n'''{msg}\n'''\n\nwithin error message:\n\n'''{error:?}'''")
507 }
508
509 #[test]
510 fn test_cranelift_flags_mismatch() -> Result<()> {
511 let engine = Engine::default();
512 let mut metadata = Metadata::new(&engine)?;
513
514 metadata
515 .shared_flags
516 .push(("preserve_frame_pointers", FlagValue::Bool(false)));
517
518 match metadata.check_compatible(&engine) {
519 Ok(_) => unreachable!(),
520 Err(e) => {
521 assert_contains(
522 &e,
523 "compilation settings of module incompatible with native host",
524 );
525 assert_contains(
526 &e,
527 "setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported",
528 );
529 }
530 }
531
532 Ok(())
533 }
534
535 #[test]
536 fn test_isa_flags_mismatch() -> Result<()> {
537 let engine = Engine::default();
538 let mut metadata = Metadata::new(&engine)?;
539
540 metadata
541 .isa_flags
542 .push(("not_a_flag", FlagValue::Bool(true)));
543
544 match metadata.check_compatible(&engine) {
545 Ok(_) => unreachable!(),
546 Err(e) => {
547 assert_contains(
548 &e,
549 "compilation settings of module incompatible with native host",
550 );
551 assert_contains(
552 &e,
553 "don't know how to test for target-specific flag \"not_a_flag\" at runtime",
554 );
555 }
556 }
557
558 Ok(())
559 }
560
561 #[test]
562 #[cfg_attr(miri, ignore)]
563 #[cfg(target_pointer_width = "64")] fn test_tunables_int_mismatch() -> Result<()> {
565 let engine = Engine::default();
566 let mut metadata = Metadata::new(&engine)?;
567
568 metadata.tunables.memory_guard_size = 0;
569
570 match metadata.check_compatible(&engine) {
571 Ok(_) => unreachable!(),
572 Err(e) => assert_eq!(
573 e.to_string(),
574 "Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
575 ),
576 }
577
578 Ok(())
579 }
580
581 #[test]
582 fn test_tunables_bool_mismatch() -> Result<()> {
583 let mut config = Config::new();
584 config.epoch_interruption(true);
585
586 let engine = Engine::new(&config)?;
587 let mut metadata = Metadata::new(&engine)?;
588 metadata.tunables.epoch_interruption = false;
589
590 match metadata.check_compatible(&engine) {
591 Ok(_) => unreachable!(),
592 Err(e) => assert_eq!(
593 e.to_string(),
594 "Module was compiled without epoch interruption but it is enabled for the host"
595 ),
596 }
597
598 let mut config = Config::new();
599 config.epoch_interruption(false);
600
601 let engine = Engine::new(&config)?;
602 let mut metadata = Metadata::new(&engine)?;
603 metadata.tunables.epoch_interruption = true;
604
605 match metadata.check_compatible(&engine) {
606 Ok(_) => unreachable!(),
607 Err(e) => assert_eq!(
608 e.to_string(),
609 "Module was compiled with epoch interruption but it is not enabled for the host"
610 ),
611 }
612
613 Ok(())
614 }
615
616 #[test]
618 #[cfg(all(target_arch = "x86_64", not(miri)))]
619 fn test_feature_mismatch() -> Result<()> {
620 let mut config = Config::new();
621 config.wasm_threads(true);
622
623 let engine = Engine::new(&config)?;
624 let mut metadata = Metadata::new(&engine)?;
625 metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
626
627 metadata.check_compatible(&engine)?;
630
631 let mut config = Config::new();
632 config.wasm_threads(false);
633
634 let engine = Engine::new(&config)?;
635 let mut metadata = Metadata::new(&engine)?;
636 metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
637
638 match metadata.check_compatible(&engine) {
639 Ok(_) => unreachable!(),
640 Err(e) => assert_eq!(
641 e.to_string(),
642 "Module was compiled with support for WebAssembly feature \
643 `threads` but it is not enabled for the host"
644 ),
645 }
646
647 Ok(())
648 }
649
650 #[test]
651 fn engine_weak_upgrades() {
652 let engine = Engine::default();
653 let weak = engine.weak();
654 weak.upgrade()
655 .expect("engine is still alive, so weak reference can upgrade");
656 drop(engine);
657 assert!(
658 weak.upgrade().is_none(),
659 "engine was dropped, so weak reference cannot upgrade"
660 );
661 }
662
663 #[test]
664 #[cfg_attr(miri, ignore)]
665 fn cache_accounts_for_opt_level() -> Result<()> {
666 let _ = env_logger::try_init();
667
668 let td = TempDir::new()?;
669 let config_path = td.path().join("config.toml");
670 std::fs::write(
671 &config_path,
672 &format!(
673 "
674 [cache]
675 directory = '{}'
676 ",
677 td.path().join("cache").display()
678 ),
679 )?;
680 let mut cfg = Config::new();
681 cfg.cranelift_opt_level(OptLevel::None)
682 .cache(Some(Cache::from_file(Some(&config_path))?));
683 let engine = Engine::new(&cfg)?;
684 Module::new(&engine, "(module (func))")?;
685 let cache_config = engine
686 .config()
687 .cache
688 .as_ref()
689 .expect("Missing cache config");
690 assert_eq!(cache_config.cache_hits(), 0);
691 assert_eq!(cache_config.cache_misses(), 1);
692 Module::new(&engine, "(module (func))")?;
693 assert_eq!(cache_config.cache_hits(), 1);
694 assert_eq!(cache_config.cache_misses(), 1);
695
696 let mut cfg = Config::new();
697 cfg.cranelift_opt_level(OptLevel::Speed)
698 .cache(Some(Cache::from_file(Some(&config_path))?));
699 let engine = Engine::new(&cfg)?;
700 let cache_config = engine
701 .config()
702 .cache
703 .as_ref()
704 .expect("Missing cache config");
705 Module::new(&engine, "(module (func))")?;
706 assert_eq!(cache_config.cache_hits(), 0);
707 assert_eq!(cache_config.cache_misses(), 1);
708 Module::new(&engine, "(module (func))")?;
709 assert_eq!(cache_config.cache_hits(), 1);
710 assert_eq!(cache_config.cache_misses(), 1);
711
712 let mut cfg = Config::new();
713 cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
714 .cache(Some(Cache::from_file(Some(&config_path))?));
715 let engine = Engine::new(&cfg)?;
716 let cache_config = engine
717 .config()
718 .cache
719 .as_ref()
720 .expect("Missing cache config");
721 Module::new(&engine, "(module (func))")?;
722 assert_eq!(cache_config.cache_hits(), 0);
723 assert_eq!(cache_config.cache_misses(), 1);
724 Module::new(&engine, "(module (func))")?;
725 assert_eq!(cache_config.cache_hits(), 1);
726 assert_eq!(cache_config.cache_misses(), 1);
727
728 let mut cfg = Config::new();
729 cfg.debug_info(true)
730 .cache(Some(Cache::from_file(Some(&config_path))?));
731 let engine = Engine::new(&cfg)?;
732 let cache_config = engine
733 .config()
734 .cache
735 .as_ref()
736 .expect("Missing cache config");
737 Module::new(&engine, "(module (func))")?;
738 assert_eq!(cache_config.cache_hits(), 0);
739 assert_eq!(cache_config.cache_misses(), 1);
740 Module::new(&engine, "(module (func))")?;
741 assert_eq!(cache_config.cache_hits(), 1);
742 assert_eq!(cache_config.cache_misses(), 1);
743
744 Ok(())
745 }
746
747 #[test]
748 fn precompile_compatibility_key_accounts_for_opt_level() {
749 fn hash_for_config(cfg: &Config) -> u64 {
750 let engine = Engine::new(cfg).expect("Config should be valid");
751 let mut hasher = DefaultHasher::new();
752 engine.precompile_compatibility_hash().hash(&mut hasher);
753 hasher.finish()
754 }
755 let mut cfg = Config::new();
756 cfg.cranelift_opt_level(OptLevel::None);
757 let opt_none_hash = hash_for_config(&cfg);
758 cfg.cranelift_opt_level(OptLevel::Speed);
759 let opt_speed_hash = hash_for_config(&cfg);
760 assert_ne!(opt_none_hash, opt_speed_hash)
761 }
762
763 #[test]
764 fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
765 fn hash_for_config(cfg: &Config) -> u64 {
766 let engine = Engine::new(cfg).expect("Config should be valid");
767 let mut hasher = DefaultHasher::new();
768 engine.precompile_compatibility_hash().hash(&mut hasher);
769 hasher.finish()
770 }
771 let mut cfg_custom_version = Config::new();
772 cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
773 let custom_version_hash = hash_for_config(&cfg_custom_version);
774
775 let mut cfg_default_version = Config::new();
776 cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
777 let default_version_hash = hash_for_config(&cfg_default_version);
778
779 let mut cfg_none_version = Config::new();
780 cfg_none_version.module_version(ModuleVersionStrategy::None)?;
781 let none_version_hash = hash_for_config(&cfg_none_version);
782
783 assert_ne!(custom_version_hash, default_version_hash);
784 assert_ne!(custom_version_hash, none_version_hash);
785 assert_ne!(default_version_hash, none_version_hash);
786
787 Ok(())
788 }
789
790 #[test]
791 #[cfg_attr(miri, ignore)]
792 #[cfg(feature = "component-model")]
793 fn components_are_cached() -> Result<()> {
794 use crate::component::Component;
795
796 let td = TempDir::new()?;
797 let config_path = td.path().join("config.toml");
798 std::fs::write(
799 &config_path,
800 &format!(
801 "
802 [cache]
803 directory = '{}'
804 ",
805 td.path().join("cache").display()
806 ),
807 )?;
808 let mut cfg = Config::new();
809 cfg.cache(Some(Cache::from_file(Some(&config_path))?));
810 let engine = Engine::new(&cfg)?;
811 let cache_config = engine
812 .config()
813 .cache
814 .as_ref()
815 .expect("Missing cache config");
816 Component::new(&engine, "(component (core module (func)))")?;
817 assert_eq!(cache_config.cache_hits(), 0);
818 assert_eq!(cache_config.cache_misses(), 1);
819 Component::new(&engine, "(component (core module (func)))")?;
820 assert_eq!(cache_config.cache_hits(), 1);
821 assert_eq!(cache_config.cache_misses(), 1);
822
823 Ok(())
824 }
825}