1 use std::collections::HashMap;
2 use std::env;
3 use std::ops::Deref;
4 use std::path::Path;
5 use std::sync::{Arc, LazyLock, Once};
6 use std::time::Duration;
7
8 use component_async_tests::{Ctx, yield_};
9 use futures::stream::{FuturesUnordered, TryStreamExt};
10 use tokio::fs;
11 use tokio::sync::Mutex;
12 use wasm_compose::composer::ComponentComposer;
13 use wasmtime::component::{Component, Linker, ResourceTable};
14 use wasmtime::{Config, Engine, Result, Store, ToWasmtimeResult as _, bail, format_err};
15 use wasmtime_wasi::WasiCtxBuilder;
16
init_logger()17 pub fn init_logger() {
18 static ONCE: Once = Once::new();
19 ONCE.call_once(env_logger::init);
20 }
21
config() -> Config22 pub fn config() -> Config {
23 init_logger();
24
25 let mut config = Config::new();
26 if env::var_os("MIRI_TEST_CWASM_DIR").is_some() {
27 config.target("pulley64").unwrap();
28 config.memory_reservation(1 << 20);
29 config.memory_guard_size(0);
30 config.signals_based_traps(false);
31 } else {
32 config.cranelift_debug_verifier(true);
33 config.cranelift_wasmtime_debug_checks(true);
34 }
35 config.wasm_component_model(true);
36 config.wasm_component_model_async(true);
37 config.wasm_component_model_async_builtins(true);
38 config.wasm_component_model_async_stackful(true);
39 config.wasm_component_model_threading(true);
40 config.wasm_component_model_error_context(true);
41 config
42 }
43
44 /// Compose two components
45 ///
46 /// a is the "root" component, and b is composed into it
compose(a: &[u8], b: &[u8]) -> Result<Vec<u8>>47 async fn compose(a: &[u8], b: &[u8]) -> Result<Vec<u8>> {
48 let dir = tempfile::tempdir()?;
49
50 let a_file = dir.path().join("a.wasm");
51 fs::write(&a_file, a).await?;
52
53 let b_file = dir.path().join("b.wasm");
54 fs::write(&b_file, b).await?;
55
56 ComponentComposer::new(
57 &a_file,
58 &wasm_compose::config::Config {
59 dir: dir.path().to_owned(),
60 definitions: vec![b_file.to_owned()],
61 ..Default::default()
62 },
63 )
64 .compose()
65 .to_wasmtime_result()
66 }
67
make_component(engine: &Engine, components: &[&str]) -> Result<Component>68 pub async fn make_component(engine: &Engine, components: &[&str]) -> Result<Component> {
69 fn cwasm_name(components: &[&str]) -> Result<String> {
70 if components.is_empty() {
71 Err(format_err!("expected at least one path"))
72 } else {
73 let names = components
74 .iter()
75 .map(|&path| {
76 let path = Path::new(path);
77 if let Some(name) = path.file_name() {
78 Ok(name)
79 } else {
80 Err(format_err!(
81 "expected path with at least two components; got: {}",
82 path.display()
83 ))
84 }
85 })
86 .collect::<Result<Vec<_>>>()?;
87
88 Ok(format!(
89 "{}.cwasm",
90 names
91 .iter()
92 .map(|name| { name.to_str().unwrap() })
93 .collect::<Vec<_>>()
94 .join("+")
95 ))
96 }
97 }
98
99 async fn compile(engine: &Engine, components: &[&str]) -> Result<Vec<u8>> {
100 let mut composed = None::<Vec<u8>>;
101 for component in components {
102 let component = fs::read(component).await?;
103 if let Some(other) = composed.take() {
104 composed = Some(compose(&other, &component).await?);
105 } else {
106 composed = Some(component);
107 }
108 }
109 engine.precompile_component(
110 &composed.ok_or_else(|| format_err!("expected at least one component"))?,
111 )
112 }
113
114 async fn load(engine: &Engine, components: &[&str]) -> Result<Vec<u8>> {
115 let cwasm_path = if let Some(cwasm_dir) = &env::var_os("MIRI_TEST_CWASM_DIR") {
116 Some(Path::new(cwasm_dir).join(cwasm_name(components)?))
117 } else {
118 None
119 };
120
121 if let Some(cwasm_path) = &cwasm_path {
122 if let Ok(compiled) = fs::read(cwasm_path).await {
123 return Ok(compiled);
124 }
125 }
126
127 if cfg!(miri) {
128 bail!(
129 "Running these tests with miri requires precompiled .cwasm files.\n\
130 Please set the `MIRI_TEST_CWASM_DIR` environment variable to the\n\
131 absolute path of a valid directory, then run the test(s)\n\
132 _without_ miri, and finally run them again _with_ miri."
133 )
134 }
135
136 let compiled = compile(engine, components).await?;
137 if let Some(cwasm_path) = &cwasm_path {
138 fs::write(cwasm_path, &compiled).await?;
139 }
140 Ok(compiled)
141 }
142
143 static CACHE: LazyLock<Mutex<HashMap<Vec<String>, Arc<Mutex<Option<Arc<Vec<u8>>>>>>>> =
144 LazyLock::new(|| Mutex::new(HashMap::new()));
145
146 let compiled = {
147 let entry = CACHE
148 .lock()
149 .await
150 .entry(components.iter().map(|&s| s.to_owned()).collect())
151 .or_insert_with(|| Arc::new(Mutex::new(None)))
152 .clone();
153
154 let mut entry = entry.lock().await;
155 if let Some(component) = entry.deref() {
156 component.clone()
157 } else {
158 let component = Arc::new(load(engine, components).await?);
159 *entry = Some(component.clone());
160 component
161 }
162 };
163
164 Ok(unsafe { Component::deserialize(&engine, &*compiled)? })
165 }
166
test_run(components: &[&str]) -> Result<()>167 pub async fn test_run(components: &[&str]) -> Result<()> {
168 test_run_with_count(components, 3).await
169 }
170
test_run_with_count(components: &[&str], count: usize) -> Result<()>171 pub async fn test_run_with_count(components: &[&str], count: usize) -> Result<()> {
172 let mut config = config();
173 // As of this writing, miri/pulley/epochs is a problematic combination, so
174 // we don't test it.
175 if env::var_os("MIRI_TEST_CWASM_DIR").is_none() {
176 config.epoch_interruption(true);
177 }
178
179 let engine = Engine::new(&config)?;
180
181 let component = make_component(&engine, components).await?;
182
183 let mut linker = Linker::new(&engine);
184
185 wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
186 component_async_tests::yield_runner::bindings::local::local::continue_::add_to_linker::<_, Ctx>(
187 &mut linker,
188 |ctx| ctx,
189 )?;
190 component_async_tests::yield_runner::bindings::local::local::ready::add_to_linker::<_, Ctx>(
191 &mut linker,
192 |ctx| ctx,
193 )?;
194 component_async_tests::resource_stream::bindings::local::local::resource_stream::add_to_linker::<
195 _,
196 Ctx,
197 >(&mut linker, |ctx| ctx)?;
198 yield_::local::local::yield_::add_to_linker::<_, Ctx>(&mut linker, |ctx| ctx)?;
199
200 let mut store = Store::new(
201 &engine,
202 Ctx {
203 wasi: WasiCtxBuilder::new().inherit_stdio().build(),
204 table: ResourceTable::default(),
205 continue_: false,
206 },
207 );
208
209 if env::var_os("MIRI_TEST_CWASM_DIR").is_none() {
210 store.set_epoch_deadline(1);
211
212 std::thread::spawn(move || {
213 std::thread::sleep(Duration::from_secs(10));
214 engine.increment_epoch();
215 });
216 }
217
218 let yield_runner =
219 component_async_tests::yield_runner::bindings::YieldRunner::instantiate_async(
220 &mut store, &component, &linker,
221 )
222 .await?;
223
224 // Start `count` concurrent calls and then join them all:
225 store
226 .run_concurrent(async |store| {
227 let mut futures = FuturesUnordered::new();
228 for _ in 0..count {
229 futures.push(yield_runner.local_local_run().call_run(store));
230 }
231
232 while let Some(()) = futures.try_next().await? {
233 // continue
234 }
235 wasmtime::error::Ok(())
236 })
237 .await??;
238
239 Ok(())
240 }
241