Skip to content
This repository was archived by the owner on Oct 3, 2025. It is now read-only.

Commit 250402c

Browse files
committed
ci: add test for suspending and resuming wasm code
1 parent b66cd50 commit 250402c

File tree

1 file changed

+377
-0
lines changed

1 file changed

+377
-0
lines changed

tests/wasm_resume.rs

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
use core::panic;
2+
use eyre;
3+
use std::sync;
4+
use std::{
5+
ops::ControlFlow,
6+
time::{Duration, Instant},
7+
};
8+
use tinywasm::{
9+
CoroState, CoroStateResumeResult, Module, ModuleInstance, PotentialCoroCallResult, Store, SuspendConditions,
10+
SuspendReason,
11+
};
12+
use tinywasm::{Extern, Imports};
13+
use wat;
14+
15+
fn main() -> std::result::Result<(), eyre::Report> {
16+
println!("\n# testing with callback");
17+
let mut cb_cond = |store: &mut Store| {
18+
let callback = make_suspend_in_time_cb(30);
19+
store.set_suspend_conditions(SuspendConditions { suspend_cb: Some(Box::new(callback)), ..Default::default() });
20+
};
21+
suspend_with_pure_loop(&mut cb_cond, SuspendReason::SuspendedCallback)?;
22+
suspend_with_wasm_fn(&mut cb_cond, SuspendReason::SuspendedCallback)?;
23+
suspend_with_host_fn(&mut cb_cond, SuspendReason::SuspendedCallback)?;
24+
25+
println!("\n# testing with epoch");
26+
let mut time_cond = |store: &mut Store| {
27+
store.set_suspend_conditions(SuspendConditions {
28+
timeout_instant: Some(Instant::now() + Duration::from_millis(10)),
29+
..Default::default()
30+
})
31+
};
32+
suspend_with_pure_loop(&mut time_cond, SuspendReason::SuspendedEpoch)?;
33+
suspend_with_wasm_fn(&mut time_cond, SuspendReason::SuspendedEpoch)?;
34+
suspend_with_host_fn(&mut time_cond, SuspendReason::SuspendedEpoch)?;
35+
36+
println!("\n# testing atomic bool");
37+
let mut cb_thead = |store: &mut Store| {
38+
let arc = sync::Arc::<sync::atomic::AtomicBool>::new(sync::atomic::AtomicBool::new(false));
39+
store.set_suspend_conditions(SuspendConditions { suspend_flag: Some(arc.clone()), ..Default::default() });
40+
let handle = std::thread::spawn(move || {
41+
std::thread::sleep(Duration::from_millis(10));
42+
arc.store(true, sync::atomic::Ordering::Release);
43+
});
44+
drop(handle);
45+
};
46+
suspend_with_pure_loop(&mut cb_thead, SuspendReason::SuspendedFlag)?;
47+
suspend_with_wasm_fn(&mut cb_thead, SuspendReason::SuspendedFlag)?;
48+
suspend_with_host_fn(&mut cb_thead, SuspendReason::SuspendedFlag)?;
49+
50+
Ok(())
51+
}
52+
53+
fn make_suspend_in_time_cb(milis: u64) -> impl FnMut(&Store) -> ControlFlow<(), ()> {
54+
let mut counter = 0 as u64;
55+
move |_| -> ControlFlow<(), ()> {
56+
counter += 1;
57+
if counter > milis {
58+
counter = 0;
59+
ControlFlow::Break(())
60+
} else {
61+
ControlFlow::Continue(())
62+
}
63+
}
64+
}
65+
66+
fn try_compare(lhs: &SuspendReason, rhs: &SuspendReason) -> eyre::Result<bool> {
67+
Ok(match lhs {
68+
SuspendReason::Yield(_) => eyre::bail!("Can't compare yields"),
69+
SuspendReason::SuspendedEpoch => matches!(rhs, SuspendReason::SuspendedEpoch),
70+
SuspendReason::SuspendedCallback => matches!(rhs, SuspendReason::SuspendedCallback),
71+
SuspendReason::SuspendedFlag => matches!(rhs, SuspendReason::SuspendedFlag),
72+
})
73+
}
74+
75+
// check if you can suspend while looping
76+
fn suspend_with_pure_loop(
77+
set_cond: &mut impl FnMut(&mut Store) -> (),
78+
expected_reason: SuspendReason,
79+
) -> eyre::Result<()> {
80+
println!("## test suspend in loop");
81+
82+
let wasm: String = {
83+
let detect_overflow = overflow_detect_snippet("$res");
84+
format!(
85+
r#"(module
86+
(memory $mem 1)
87+
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 4 - overflow flag
88+
89+
(func (export "start_counter")
90+
(local $res i64)
91+
(loop $lp
92+
(i32.const 0) ;;where to store
93+
(i64.load $mem (i32.const 0))
94+
(i64.const 1)
95+
(i64.add)
96+
(local.set $res)
97+
(local.get $res)
98+
(i64.store $mem)
99+
{detect_overflow}
100+
(br $lp)
101+
)
102+
)
103+
)"#
104+
)
105+
.into()
106+
};
107+
108+
let mut tested = {
109+
let wasm = wat::parse_str(wasm)?;
110+
let module = Module::parse_bytes(&wasm)?;
111+
let mut store = Store::default();
112+
let instance = module.instantiate(&mut store, None)?;
113+
TestedModule { store, instance: instance, resumable: None }
114+
};
115+
116+
let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
117+
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");
118+
Ok(())
119+
}
120+
121+
// check if you can suspend when calling wasm function
122+
fn suspend_with_wasm_fn(
123+
set_cond: &mut impl FnMut(&mut Store) -> (),
124+
expected_reason: SuspendReason,
125+
) -> eyre::Result<()> {
126+
println!("## test suspend wasm fn");
127+
128+
let wasm: String = {
129+
let detect_overflow = overflow_detect_snippet("$res");
130+
format!(
131+
r#"(module
132+
(memory $mem 1)
133+
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 8 - overflow counter
134+
135+
(func $wasm_nop
136+
nop
137+
)
138+
139+
(func $wasm_adder (param i64 i64) (result i64)
140+
(local.get 0)
141+
(local.get 1)
142+
(i64.add)
143+
)
144+
145+
(func $overflow_detect (param $res i64)
146+
{detect_overflow}
147+
)
148+
149+
(func (export "start_counter")
150+
(local $res i64)
151+
(loop $lp
152+
(call $wasm_nop)
153+
(i32.const 0) ;;where to store
154+
(i64.load $mem (i32.const 0))
155+
(i64.const 1)
156+
(call $wasm_adder)
157+
(local.set $res)
158+
(call $wasm_nop)
159+
(local.get $res)
160+
(i64.store $mem)
161+
(local.get $res)
162+
(call $overflow_detect)
163+
(call $wasm_nop)
164+
(br $lp)
165+
)
166+
)
167+
)"#
168+
)
169+
.into()
170+
};
171+
172+
let mut tested = {
173+
let wasm = wat::parse_str(wasm)?;
174+
let module = Module::parse_bytes(&wasm)?;
175+
let mut store = Store::default();
176+
let instance = module.instantiate(&mut store, None)?;
177+
TestedModule { store, instance: instance, resumable: None }
178+
};
179+
180+
let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
181+
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");
182+
183+
Ok(())
184+
}
185+
186+
// check if you can suspend when calling host function
187+
fn suspend_with_host_fn(
188+
set_cond: &mut impl FnMut(&mut Store) -> (),
189+
expected_reason: SuspendReason,
190+
) -> eyre::Result<()> {
191+
println!("## test suspend host fn");
192+
193+
let wasm: String = {
194+
format!(
195+
r#"(module
196+
(import "host" "adder" (func $host_adder (param i64 i64)(result i64)))
197+
(import "host" "nop" (func $host_nop))
198+
(import "host" "detect" (func $overflow_detect (param $res i64)))
199+
(memory $mem 1)
200+
(export "memory" (memory $mem)) ;; first 8 bytes - counter, next 8 - overflow counter
201+
202+
(func (export "start_counter")
203+
(local $res i64)
204+
(loop $lp
205+
(call $host_nop)
206+
(i32.const 0) ;;where to store
207+
(i64.load $mem (i32.const 0))
208+
(i64.const 1)
209+
(call $host_adder)
210+
(local.set $res)
211+
(call $host_nop)
212+
(local.get $res)
213+
(i64.store $mem)
214+
(local.get $res)
215+
(call $overflow_detect)
216+
(call $host_nop)
217+
(br $lp)
218+
)
219+
)
220+
)"#
221+
)
222+
.into()
223+
};
224+
225+
let mut tested = {
226+
let wasm = wat::parse_str(wasm)?;
227+
let module = Module::parse_bytes(&wasm)?;
228+
let mut store = Store::default();
229+
let mut imports = Imports::new();
230+
imports.define(
231+
"host",
232+
"adder",
233+
Extern::typed_func(|_, args: (i64, i64)| -> tinywasm::Result<i64> { Ok(args.0 + args.1) }),
234+
)?;
235+
imports.define(
236+
"host",
237+
"nop",
238+
Extern::typed_func(|_, ()| -> tinywasm::Result<()> {
239+
std::thread::sleep(Duration::from_micros(1));
240+
Ok(())
241+
}),
242+
)?;
243+
imports.define(
244+
"host",
245+
"detect",
246+
Extern::typed_func(|mut ctx, arg: i64| -> tinywasm::Result<()> {
247+
if arg != 0 {
248+
return Ok(());
249+
}
250+
let mut mem = ctx.module().exported_memory_mut(ctx.store_mut(), "memory").expect("where's memory");
251+
let mut buf = [0 as u8; 8];
252+
buf.copy_from_slice(mem.load(8, 8)?);
253+
let counter = i64::from_be_bytes(buf);
254+
mem.store(8, 8, &i64::to_be_bytes(counter + 1))?;
255+
Ok(())
256+
}),
257+
)?;
258+
259+
let instance = module.instantiate(&mut store, Some(imports))?;
260+
TestedModule { store, instance: instance, resumable: None }
261+
};
262+
263+
let increases = run_loops_look_at_numbers(&mut tested, set_cond, expected_reason, 16)?;
264+
assert!(increases > 2, "code doesn't enough: either suspend condition is too tight or something is broken");
265+
Ok(())
266+
}
267+
268+
fn run_loops_look_at_numbers(
269+
tested: &mut TestedModule,
270+
set_cond: &mut impl FnMut(&mut Store) -> (),
271+
expected_reason: SuspendReason,
272+
times: u32,
273+
) -> eyre::Result<u32> {
274+
set_cond(&mut tested.store);
275+
let suspend = tested.start_counter_incrementing_loop("start_counter")?;
276+
assert!(try_compare(&suspend, &expected_reason).expect("unexpected yield"));
277+
278+
let mut prev_counter = tested.get_counter();
279+
let mut times_increased = 0 as u32;
280+
281+
{
282+
let (big, small) = prev_counter;
283+
println!("after start {big} {small}");
284+
}
285+
286+
assert!(prev_counter >= (0, 0));
287+
288+
for _ in 0..times - 1 {
289+
set_cond(&mut tested.store);
290+
assert!(try_compare(&tested.continue_counter_incrementing_loop()?, &expected_reason)?);
291+
let new_counter = tested.get_counter();
292+
// save for scheduling weirdness, loop should run for a bunch of times in 3ms
293+
assert!(new_counter >= prev_counter);
294+
{
295+
let (big, small) = new_counter;
296+
println!("after continue {big} {small}");
297+
}
298+
if new_counter > prev_counter {
299+
times_increased += 1;
300+
}
301+
prev_counter = new_counter;
302+
}
303+
Ok(times_increased)
304+
}
305+
306+
fn overflow_detect_snippet(var: &str) -> String {
307+
format!(
308+
r#"(i64.eq (i64.const 0) (local.get {var}))
309+
(if
310+
(then
311+
;; we wrapped around back to 0 - set flag
312+
(i32.const 8) ;;where to store
313+
(i32.const 8) ;;where to load
314+
(i64.load)
315+
(i64.const 1)
316+
(i64.add)
317+
(i64.store $mem)
318+
)
319+
(else
320+
nop
321+
)
322+
)
323+
"#
324+
)
325+
.into()
326+
}
327+
328+
// should have exported memory "memory" and
329+
struct TestedModule {
330+
store: Store,
331+
instance: ModuleInstance,
332+
resumable: Option<tinywasm::SuspendFunc>,
333+
}
334+
335+
impl TestedModule {
336+
fn start_counter_incrementing_loop(&mut self, fn_name: &str) -> tinywasm::Result<SuspendReason> {
337+
let starter = self.instance.exported_func_untyped(&self.store, fn_name)?;
338+
if let PotentialCoroCallResult::Suspended(res, coro) = starter.call_coro(&mut self.store, &[])? {
339+
self.resumable = Some(coro);
340+
return Ok(res);
341+
} else {
342+
panic!("that should never return");
343+
}
344+
}
345+
346+
fn continue_counter_incrementing_loop(&mut self) -> tinywasm::Result<SuspendReason> {
347+
let paused = if let Some(val) = self.resumable.as_mut() {
348+
val
349+
} else {
350+
panic!("nothing to continue");
351+
};
352+
let resume_res = (*paused).resume(&mut self.store, None)?;
353+
match resume_res {
354+
CoroStateResumeResult::Suspended(res) => Ok(res),
355+
CoroStateResumeResult::Return(_) => panic!("should never return"),
356+
}
357+
}
358+
359+
// (counter, overflow flag)
360+
fn get_counter(&self) -> (u64, u64) {
361+
let counter_now = {
362+
let mem = self.instance.exported_memory(&self.store, "memory").expect("where's memory");
363+
let mut buff: [u8; 8] = [0; 8];
364+
let in_mem = mem.load(0, 8).expect("where's memory");
365+
buff.clone_from_slice(in_mem);
366+
u64::from_le_bytes(buff)
367+
};
368+
let overflow_times = {
369+
let mem = self.instance.exported_memory(&self.store, "memory").expect("where's memory");
370+
let mut buff: [u8; 8] = [0; 8];
371+
let in_mem = mem.load(8, 8).expect("where's memory");
372+
buff.clone_from_slice(in_mem);
373+
u64::from_le_bytes(buff)
374+
};
375+
(overflow_times, counter_now)
376+
}
377+
}

0 commit comments

Comments
 (0)