Skip to content

Commit c3bac23

Browse files
yoshuawuytsCopilot
andcommitted
Run p3 integration tests in CI via a harness-free test_main!
The `wstd tests (wasip3)` CI step was broken: p3's `block_on` blocks on `waitable-set.wait`, which traps ("cannot block a synchronous task before returning") when driven from libtest's synchronously-lifted `wasi:cli/run` `main`. p2's `block_on` blocks via `wasi:io/poll.poll`, which a sync task may do, so only p3 was affected. Add a dependency-free `wstd::test_main!` macro (plus a `#[doc(hidden)]` `__test` support module) that provides a `harness = false` entry point for integration tests: * on p3 it async-lifts the test binary's `wasi:cli/run` export, so per-test `block_on` calls are made from an async task and no longer trap; * on p2 it emits a plain `fn main` that runs each test via `block_on`. Both variants print libtest-style status lines and exit non-zero when a test fails (whether it panics or returns `Err`), so `cargo test` still reports failures. Convert all integration tests to plain `async fn`s driven by `test_main!` and mark each `[[test]]` target `harness = false`. To let a single `wasm32-wasip2` build serve both backends, add the component-model async runner flags (`-Wcomponent-model-async`, `-Wcomponent-model-more-async-builtins`, `-Wcomponent-model-async-stackful`, `-Sp3`) to the `wasm32-wasip2` runner. These are inert for plain p2 components, so the wasip2 suite is unaffected. Finally, gate the two blocking `src/time.rs` unit tests (`timer_now`, `timer_after_100_milliseconds`) with `#[cfg_attr(wstd_p3, ignore)]`: they run under libtest's default (synchronous) harness in the `[lib]` target, which still traps on p3. Their behavior is covered by the integration test suite. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent db6a4cc commit c3bac23

13 files changed

Lines changed: 229 additions & 18 deletions

.cargo/config.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
[target.wasm32-wasip2]
22
# wasmtime is given:
3+
# * component-model async support so p3 test/example components (built for the
4+
# wasip2 target with `--features wasip3`) validate and can async-lift their
5+
# `wasi:cli/run` export. These flags are inert for plain p2 components.
6+
# * p3 enabled so p3 components can import the WASI 0.3 interfaces.
37
# * http enabled for wasi-http tests
48
# * AWS auth environment variables, for running the wstd-aws integration tests.
59
# * . directory is available at .
6-
runner = "wasmtime run -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::."
10+
runner = "wasmtime run -Wcomponent-model-async -Wcomponent-model-more-async-builtins -Wcomponent-model-async-stackful -Sp3 -Shttp --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --dir .::."
711

812
[target.wasm32-wasip3]
913
# wasmtime is given:

Cargo.toml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,50 @@ humantime.workspace = true
5353
serde = { workspace = true, features = ["derive"] }
5454
serde_json.workspace = true
5555

56+
# Integration tests use `wstd::test_main!` with a custom `harness = false`
57+
# entry point. This is required for p3, where the test binary must async-lift
58+
# its `wasi:cli/run` export (libtest lifts `main` synchronously, which cannot
59+
# block on async imports). See `wstd::test_main!` for details.
60+
[[test]]
61+
name = "timer"
62+
harness = false
63+
64+
[[test]]
65+
name = "sleep"
66+
harness = false
67+
68+
[[test]]
69+
name = "stdio"
70+
harness = false
71+
72+
[[test]]
73+
name = "http_get"
74+
harness = false
75+
76+
[[test]]
77+
name = "http_get_json"
78+
harness = false
79+
80+
[[test]]
81+
name = "http_post"
82+
harness = false
83+
84+
[[test]]
85+
name = "http_post_json"
86+
harness = false
87+
88+
[[test]]
89+
name = "http_handle_error_code"
90+
harness = false
91+
92+
[[test]]
93+
name = "http_timeout"
94+
harness = false
95+
96+
[[test]]
97+
name = "http_first_byte_timeout"
98+
harness = false
99+
56100
[workspace]
57101
members = [
58102
"axum",

src/lib.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,52 @@ pub mod __internal {
8989
pub use wasip2;
9090
}
9191

92+
/// Test-harness support used by the [`test_main!`](crate::test_main) macro.
93+
///
94+
/// This is `#[doc(hidden)]` and not part of the public API.
95+
#[doc(hidden)]
96+
pub mod __test {
97+
/// Conversion from a test function's return type into a pass/fail result.
98+
///
99+
/// Implemented for `()` and `Result<(), E: Debug>`, matching the return
100+
/// types permitted on the `async fn` tests collected by [`test_main!`].
101+
///
102+
/// [`test_main!`]: crate::test_main
103+
pub trait TestOutcome {
104+
/// Convert the outcome into `Ok(())` on success or an `Err` carrying a
105+
/// rendered failure message.
106+
fn into_test_result(self) -> Result<(), String>;
107+
}
108+
109+
impl TestOutcome for () {
110+
fn into_test_result(self) -> Result<(), String> {
111+
Ok(())
112+
}
113+
}
114+
115+
impl<E: core::fmt::Debug> TestOutcome for Result<(), E> {
116+
fn into_test_result(self) -> Result<(), String> {
117+
self.map_err(|err| format!("{err:?}"))
118+
}
119+
}
120+
121+
/// Report one test's outcome with a libtest-style status line, returning
122+
/// `true` when the test passed.
123+
pub fn report(name: &str, outcome: impl TestOutcome) -> bool {
124+
match outcome.into_test_result() {
125+
Ok(()) => {
126+
println!("test {name} ... ok");
127+
true
128+
}
129+
Err(err) => {
130+
println!("test {name} ... FAILED");
131+
eprintln!("---- {name} ----\n{err}\n");
132+
false
133+
}
134+
}
135+
}
136+
}
137+
92138
// Conditionally-compiled declarative macro for the `#[wstd::main]` entry point.
93139
//
94140
// The `#[wstd::main]` proc macro delegates to this declarative macro so the
@@ -141,6 +187,88 @@ macro_rules! __main_export {
141187
};
142188
}
143189

190+
/// Define the entry point of an integration-test binary composed of `async fn`
191+
/// tests.
192+
///
193+
/// On p3 a test cannot use the standard libtest harness: libtest lifts its
194+
/// `main` as a *synchronous* `wasi:cli/run` task, and a synchronous task may
195+
/// not block on async-lowered imports (it traps with "cannot block a
196+
/// synchronous task before returning"). Instead, set `harness = false` for the
197+
/// test target and use this macro, which async-lifts `wasi:cli/run` on p3 and
198+
/// generates a plain `fn main` on p2. Each listed `async fn` test is driven to
199+
/// completion with [`block_on`](crate::runtime::block_on):
200+
///
201+
/// ```ignore
202+
/// async fn my_test() -> Result<(), Box<dyn std::error::Error>> {
203+
/// Ok(())
204+
/// }
205+
///
206+
/// wstd::test_main! { my_test }
207+
/// ```
208+
///
209+
/// Tests may return `()` or `Result<(), E>` where `E: Debug`. The process exits
210+
/// non-zero if any test returns an error, so `cargo test` reports the failure.
211+
#[macro_export]
212+
macro_rules! test_main {
213+
( $( $test:path ),* $(,)? ) => {
214+
$crate::__test_main_export! { $( $test ),* }
215+
};
216+
}
217+
218+
#[cfg(wstd_p2)]
219+
#[macro_export]
220+
#[doc(hidden)]
221+
macro_rules! __test_main_export {
222+
( $( $test:path ),* $(,)? ) => {
223+
fn main() {
224+
let mut all_ok = true;
225+
$(
226+
all_ok &= $crate::__test::report(
227+
::core::stringify!($test),
228+
$crate::runtime::block_on($test()),
229+
);
230+
)*
231+
if !all_ok {
232+
::std::process::exit(1);
233+
}
234+
}
235+
};
236+
}
237+
238+
#[cfg(wstd_p3)]
239+
#[macro_export]
240+
#[doc(hidden)]
241+
macro_rules! __test_main_export {
242+
( $( $test:path ),* $(,)? ) => {
243+
const _: () = {
244+
struct __WstdTestMain;
245+
246+
impl $crate::__internal::wasip3::exports::cli::run::Guest for __WstdTestMain {
247+
async fn run() -> ::core::result::Result<(), ()> {
248+
let mut all_ok = true;
249+
$(
250+
all_ok &= $crate::__test::report(
251+
::core::stringify!($test),
252+
$crate::runtime::block_on($test()),
253+
);
254+
)*
255+
if all_ok {
256+
::core::result::Result::Ok(())
257+
} else {
258+
::core::result::Result::Err(())
259+
}
260+
}
261+
}
262+
263+
$crate::__internal::wasip3::cli::command::export!(__WstdTestMain with_types_in $crate::__internal::wasip3);
264+
};
265+
266+
// The bin target still requires a `fn main`; the real entry point is the
267+
// async-lifted `wasi:cli/run` export above, so this is never invoked.
268+
fn main() {}
269+
};
270+
}
271+
144272
pub mod prelude {
145273
pub use crate::future::FutureExt as _;
146274
pub use crate::io::AsyncRead as _;

src/time.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,21 @@ mod tests {
424424
}
425425

426426
#[test]
427+
#[cfg_attr(
428+
wstd_p3,
429+
ignore = "block_on traps under libtest's synchronous harness on p3; covered by the integration test suite"
430+
)]
427431
fn timer_now() {
428432
crate::runtime::block_on(debug_duration("timer_now", async {
429433
Timer::at(Instant::now()).wait().await
430434
}));
431435
}
432436

433437
#[test]
438+
#[cfg_attr(
439+
wstd_p3,
440+
ignore = "block_on traps under libtest's synchronous harness on p3; covered by the integration test suite"
441+
)]
434442
fn timer_after_100_milliseconds() {
435443
crate::runtime::block_on(debug_duration("timer_after_100_milliseconds", async {
436444
Timer::after(Duration::from_millis(100)).wait().await

tests/http_get.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use std::error::Error;
22
use wstd::http::{Body, Client, HeaderValue, Request};
33

4-
#[wstd::test]
5-
async fn main() -> Result<(), Box<dyn Error>> {
4+
async fn http_get() -> Result<(), Box<dyn Error>> {
65
let request = Request::get("https://postman-echo.com/get")
76
.header("my-header", HeaderValue::from_str("my-value")?)
87
.body(Body::empty())?;
@@ -51,3 +50,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
5150

5251
Ok(())
5352
}
53+
54+
wstd::test_main! {
55+
http_get,
56+
}

tests/http_get_json.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ struct Echo {
77
url: String,
88
}
99

10-
#[wstd::test]
11-
async fn main() -> Result<(), Box<dyn Error>> {
10+
async fn http_get_json() -> Result<(), Box<dyn Error>> {
1211
let request = Request::get("https://postman-echo.com/get").body(Body::empty())?;
1312

1413
let response = Client::new().send(request).await?;
@@ -27,3 +26,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
2726

2827
Ok(())
2928
}
29+
30+
wstd::test_main! {
31+
http_get_json,
32+
}

tests/http_handle_error_code.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use wstd::http::{Body, Client, Request, error::ErrorCode};
22

33
/// Test that `outgoing_handler::handle` errors are properly propagated.
4-
#[wstd::test]
54
async fn handle_returns_error_code() -> Result<(), Box<dyn std::error::Error>> {
65
let request = Request::get("ftp://example.com/").body(Body::empty())?;
76

@@ -19,3 +18,7 @@ async fn handle_returns_error_code() -> Result<(), Box<dyn std::error::Error>> {
1918

2019
Ok(())
2120
}
21+
22+
wstd::test_main! {
23+
handle_returns_error_code,
24+
}

tests/http_post.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use std::error::Error;
22
use wstd::http::{Client, HeaderValue, Request};
33

4-
#[wstd::test]
5-
async fn main() -> Result<(), Box<dyn Error>> {
4+
async fn http_post() -> Result<(), Box<dyn Error>> {
65
let request = Request::post("https://postman-echo.com/post")
76
.header(
87
"content-type",
@@ -49,3 +48,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
4948

5049
Ok(())
5150
}
51+
52+
wstd::test_main! {
53+
http_post,
54+
}

tests/http_post_json.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ struct Echo {
1212
url: String,
1313
}
1414

15-
#[wstd::test]
16-
async fn main() -> Result<(), Box<dyn Error>> {
15+
async fn http_post_json() -> Result<(), Box<dyn Error>> {
1716
let test_data = TestData {
1817
test: "data".to_string(),
1918
};
@@ -41,3 +40,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
4140

4241
Ok(())
4342
}
43+
44+
wstd::test_main! {
45+
http_post_json,
46+
}

tests/http_timeout.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use wstd::future::FutureExt;
22
use wstd::http::{Body, Client, Request};
33
use wstd::time::Duration;
44

5-
#[wstd::test]
65
async fn http_timeout() -> Result<(), Box<dyn std::error::Error>> {
76
// This get request will connect to the server, which will then wait 1 second before
87
// returning a response.
@@ -21,3 +20,7 @@ async fn http_timeout() -> Result<(), Box<dyn std::error::Error>> {
2120

2221
Ok(())
2322
}
23+
24+
wstd::test_main! {
25+
http_timeout,
26+
}

0 commit comments

Comments
 (0)