1use std::collections::{HashMap, HashSet};
13use std::path::PathBuf;
14
15use wasmtime::component::ResourceTable;
16use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
17
18use yosh_plugin_api::pattern::CommandPattern;
19
20pub mod commands;
21pub mod files;
22pub mod filesystem;
23pub mod io;
24pub mod variables;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ExecRecord {
30 pub program: String,
31 pub args: Vec<String>,
32 pub exit_code: i32,
33 pub stdout_len: usize,
34 pub stderr_len: usize,
35}
36
37#[derive(Debug, Default, Clone)]
47pub struct TestState {
48 pub caps: u32,
50 pub vars: HashMap<String, String>,
51 pub exported: HashSet<String>,
52 pub cwd: PathBuf,
53 pub stdout: Vec<u8>,
54 pub stderr: Vec<u8>,
55 pub files: HashMap<PathBuf, Vec<u8>>,
57 pub sandbox_root: Option<PathBuf>,
60 pub allow_exec: Vec<CommandPattern>,
62 pub exec_log: Vec<ExecRecord>,
63 pub set_log: Vec<(String, String)>,
66 pub export_log: Vec<(String, String)>,
67 pub write_log: Vec<(PathBuf, usize)>,
69}
70
71impl TestState {
72 pub fn with_caps(caps: u32) -> Self {
77 TestState {
78 caps,
79 ..TestState::default()
80 }
81 }
82}
83
84pub struct TestCtx {
88 pub state: TestState,
89 pub(crate) table: ResourceTable,
90 pub(crate) wasi: WasiCtx,
91}
92
93impl Default for TestCtx {
94 fn default() -> Self {
95 let wasi = WasiCtxBuilder::new().build();
98 TestCtx {
99 state: TestState::default(),
100 table: ResourceTable::new(),
101 wasi,
102 }
103 }
104}
105
106impl TestCtx {
107 pub fn new(state: TestState) -> Self {
109 TestCtx {
110 state,
111 table: ResourceTable::new(),
112 wasi: WasiCtxBuilder::new().build(),
113 }
114 }
115}
116
117impl WasiView for TestCtx {
118 fn ctx(&mut self) -> &mut WasiCtx {
119 &mut self.wasi
120 }
121 fn table(&mut self) -> &mut ResourceTable {
122 &mut self.table
123 }
124}
125
126use wasmtime::Engine;
127use wasmtime::component::Linker;
128
129pub fn build_linker(engine: &Engine) -> wasmtime::Result<Linker<TestCtx>> {
132 let mut linker = Linker::<TestCtx>::new(engine);
133 register_wasi(&mut linker)?;
134 Ok(linker)
135}
136
137fn register_wasi(linker: &mut Linker<TestCtx>) -> wasmtime::Result<()> {
141 wasmtime_wasi::add_to_linker_sync(linker)
142}
143
144use crate::generated::yosh::plugin::commands::ExecOutput;
145use crate::generated::yosh::plugin::files::{DirEntry, FileStat};
146use crate::generated::yosh::plugin::types::{ErrorCode, IoStream};
147
148pub fn register_imports(linker: &mut Linker<TestCtx>) -> wasmtime::Result<()> {
152 let mut vars = linker.instance("yosh:plugin/variables@0.2.1")?;
154 vars.func_wrap(
155 "get",
156 |store: wasmtime::StoreContextMut<'_, TestCtx>, (name,): (String,)| {
157 Ok::<_, wasmtime::Error>((variables::host_get(&store.data().state, &name),))
158 },
159 )?;
160 vars.func_wrap(
161 "set",
162 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (name, value): (String, String)| {
163 Ok::<_, wasmtime::Error>((variables::host_set(
164 &mut store.data_mut().state,
165 &name,
166 &value,
167 ),))
168 },
169 )?;
170 vars.func_wrap(
171 "export-env",
172 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (name, value): (String, String)| {
173 Ok::<_, wasmtime::Error>((variables::host_export_env(
174 &mut store.data_mut().state,
175 &name,
176 &value,
177 ),))
178 },
179 )?;
180
181 let mut fs = linker.instance("yosh:plugin/filesystem@0.2.1")?;
183 fs.func_wrap(
184 "cwd",
185 |store: wasmtime::StoreContextMut<'_, TestCtx>, (): ()| {
186 Ok::<_, wasmtime::Error>((filesystem::host_cwd(&store.data().state),))
187 },
188 )?;
189 fs.func_wrap(
190 "set-cwd",
191 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path,): (String,)| {
192 Ok::<_, wasmtime::Error>(
193 (filesystem::host_set_cwd(&mut store.data_mut().state, &path),),
194 )
195 },
196 )?;
197
198 let mut io_inst = linker.instance("yosh:plugin/io@0.2.1")?;
200 io_inst.func_wrap(
201 "write",
202 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (target, data): (IoStream, Vec<u8>)| {
203 Ok::<_, wasmtime::Error>((io::host_write(&mut store.data_mut().state, target, &data),))
204 },
205 )?;
206
207 let mut f = linker.instance("yosh:plugin/files@0.2.1")?;
209 f.func_wrap(
210 "read-file",
211 |store: wasmtime::StoreContextMut<'_, TestCtx>, (path,): (String,)| {
212 Ok::<_, wasmtime::Error>((files::host_read_file(&store.data().state, &path),))
213 },
214 )?;
215 f.func_wrap(
216 "read-dir",
217 |store: wasmtime::StoreContextMut<'_, TestCtx>, (path,): (String,)| {
218 Ok::<_, wasmtime::Error>((files::host_read_dir(&store.data().state, &path),))
219 },
220 )?;
221 f.func_wrap(
222 "metadata",
223 |store: wasmtime::StoreContextMut<'_, TestCtx>, (path,): (String,)| {
224 Ok::<_, wasmtime::Error>((files::host_metadata(&store.data().state, &path),))
225 },
226 )?;
227 f.func_wrap(
228 "write-file",
229 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path, data): (String, Vec<u8>)| {
230 Ok::<_, wasmtime::Error>((files::host_write_file(
231 &mut store.data_mut().state,
232 &path,
233 &data,
234 ),))
235 },
236 )?;
237 f.func_wrap(
238 "append-file",
239 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path, data): (String, Vec<u8>)| {
240 Ok::<_, wasmtime::Error>((files::host_append_file(
241 &mut store.data_mut().state,
242 &path,
243 &data,
244 ),))
245 },
246 )?;
247 f.func_wrap(
248 "create-dir",
249 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path, recursive): (String, bool)| {
250 Ok::<_, wasmtime::Error>((files::host_create_dir(
251 &mut store.data_mut().state,
252 &path,
253 recursive,
254 ),))
255 },
256 )?;
257 f.func_wrap(
258 "remove-file",
259 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path,): (String,)| {
260 Ok::<_, wasmtime::Error>((files::host_remove_file(&mut store.data_mut().state, &path),))
261 },
262 )?;
263 f.func_wrap(
264 "remove-dir",
265 |mut store: wasmtime::StoreContextMut<'_, TestCtx>, (path, recursive): (String, bool)| {
266 Ok::<_, wasmtime::Error>((files::host_remove_dir(
267 &mut store.data_mut().state,
268 &path,
269 recursive,
270 ),))
271 },
272 )?;
273
274 let mut cmds = linker.instance("yosh:plugin/commands@0.2.1")?;
276 cmds.func_wrap(
277 "exec",
278 |mut store: wasmtime::StoreContextMut<'_, TestCtx>,
279 (program, args): (String, Vec<String>)| {
280 Ok::<_, wasmtime::Error>((commands::host_exec(
281 &mut store.data_mut().state,
282 &program,
283 &args,
284 ),))
285 },
286 )?;
287
288 let _ = (
291 std::marker::PhantomData::<ExecOutput>,
292 std::marker::PhantomData::<DirEntry>,
293 std::marker::PhantomData::<FileStat>,
294 std::marker::PhantomData::<ErrorCode>,
295 );
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_state_default_is_empty() {
305 let s = TestState::default();
306 assert!(s.vars.is_empty());
307 assert!(s.exported.is_empty());
308 assert_eq!(s.cwd.as_os_str(), "");
309 assert!(s.stdout.is_empty());
310 assert!(s.stderr.is_empty());
311 assert_eq!(s.caps, 0);
312 }
313
314 #[test]
315 fn test_ctx_default_constructs() {
316 let _ctx = TestCtx::default();
317 }
318
319 #[test]
320 fn linker_construction_smoke() {
321 let engine = crate::precompile::make_engine().expect("engine");
322 let _linker = build_linker(&engine).expect("linker");
323 }
324
325 #[test]
326 fn linker_with_yosh_imports_constructs() {
327 let engine = crate::precompile::make_engine().unwrap();
328 let mut linker = build_linker(&engine).unwrap();
329 register_imports(&mut linker).expect("yosh imports");
330 }
331}