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