Skip to content

Commit a6ec61a

Browse files
committed
test(runtime): assert guest-observed wasi:clocks time under a ManualClock override
Add a clock-reader fixture that reads the WASI wall clock through std on every event and logs it, plus a harness test that pins a ManualClock, boots the fixture under that override, dispatches a block, and asserts the guest logged the pinned instant rather than the ambient host clock. This proves the WasiClockOverride reaches the guest end to end, not just the host boot path.
1 parent a22852e commit a6ec61a

8 files changed

Lines changed: 154 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
cargo build --release --target wasm32-wasip2 --locked
5858
-p example -p twap-monitor -p ethflow-watcher -p price-alert
5959
-p balance-tracker -p stop-loss -p http-probe -p echo-venue
60-
-p echo-client -p flaky-bomb -p fuel-bomb -p memory-bomb -p panic-bomb
60+
-p echo-client -p clock-reader -p flaky-bomb -p fuel-bomb -p memory-bomb -p panic-bomb
6161
- run: cargo test --workspace --all-features --no-fail-fast --locked
6262

6363
docs:

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ members = [
2020
"modules/examples/http-probe",
2121
"modules/examples/price-alert",
2222
"modules/examples/stop-loss",
23+
"modules/fixtures/clock-reader",
2324
"modules/fixtures/flaky-bomb",
2425
"modules/fixtures/fuel-bomb",
2526
"modules/fixtures/memory-bomb",

crates/nexum-runtime/src/test_utils/harness.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,4 +609,61 @@ direction = "above"
609609
rt.shutdown();
610610
rt.wait().await.expect("clean shutdown");
611611
}
612+
613+
/// The guest observes the `WasiClockOverride` end to end: pin the harness
614+
/// clock to a known instant, boot the clock-reader fixture under it, and
615+
/// dispatch a block. The fixture reads `wasi:clocks/wall-clock` through
616+
/// `std` and logs the wall time as whole seconds, so the logged value
617+
/// equalling the pinned instant (and not the ambient host clock) proves
618+
/// the override reaches the guest, not just the host boot path.
619+
#[tokio::test]
620+
async fn harness_guest_observes_the_clock_override() {
621+
use std::time::{Duration, UNIX_EPOCH};
622+
623+
let Some(wasm) = module_wasm_or_skip("clock_reader.wasm") else {
624+
return;
625+
};
626+
627+
// A round instant far from the ambient clock: a stale ambient read
628+
// would land in the 1.7-billion-plus range of the present, so an
629+
// exact match on this value can only come from the override.
630+
const PINNED_SECS: u64 = 1_700_000_000;
631+
632+
let builder = TestRuntime::builder(wasm).manifest_inline(block_manifest("clock-reader", 1));
633+
builder
634+
.clock()
635+
.set(UNIX_EPOCH + Duration::from_secs(PINNED_SECS));
636+
637+
let mut rt = builder
638+
.launch()
639+
.await
640+
.expect("launch clock-reader over the harness");
641+
642+
rt.push_block(header_numbered(19_000_000));
643+
let record = rt
644+
.wait_for_log("clock-reader", &format!("clock wall {PINNED_SECS}"))
645+
.await
646+
.expect("the guest logs its wall-clock reading after dispatch");
647+
648+
// The line is a host-interface log carrying exactly the pinned
649+
// seconds, parsed back to guard against a substring false positive.
650+
assert_eq!(
651+
record.source,
652+
crate::host::logs::LogSource::HostInterface,
653+
"the fixture logs through the host interface",
654+
);
655+
let logged: u64 = record
656+
.message
657+
.rsplit(' ')
658+
.next()
659+
.and_then(|s| s.parse().ok())
660+
.expect("the log line ends in the wall-clock seconds");
661+
assert_eq!(
662+
logged, PINNED_SECS,
663+
"the guest read the overridden wall clock, not the ambient host clock",
664+
);
665+
666+
rt.shutdown();
667+
rt.wait().await.expect("clean shutdown");
668+
}
612669
}

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,5 @@ ci:
9898
cargo build --release --target wasm32-wasip2 \
9999
-p example -p twap-monitor -p ethflow-watcher -p price-alert \
100100
-p balance-tracker -p stop-loss -p http-probe -p echo-venue \
101-
-p echo-client -p flaky-bomb -p fuel-bomb -p memory-bomb -p panic-bomb
101+
-p echo-client -p clock-reader -p flaky-bomb -p fuel-bomb -p memory-bomb -p panic-bomb
102102
cargo test --workspace --all-features --no-fail-fast
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "clock-reader"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
7+
description = "Test fixture: on every event reads the WASI wall clock through std and logs it. Lets a test assert the guest observes a WasiClockOverride end to end."
8+
9+
[lib]
10+
crate-type = ["cdylib"]
11+
12+
[dependencies]
13+
wit-bindgen = { version = "0.58", default-features = false, features = ["macros", "realloc"] }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# clock-reader test fixture. Subscribes to a single chain's blocks so the
2+
# supervisor invokes `on_event` once per block; the handler reads the WASI
3+
# wall clock through std and logs it. The integration test boots this
4+
# fixture under a pinned clock override and asserts the logged time matches
5+
# the override, proving the guest observes virtualized time end to end.
6+
7+
[module]
8+
name = "clock-reader"
9+
version = "0.1.0"
10+
component = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
11+
12+
[capabilities]
13+
required = ["logging"]
14+
optional = []
15+
16+
[capabilities.http]
17+
allow = []
18+
19+
[[subscription]]
20+
kind = "block"
21+
chain_id = 1
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! # clock-reader (test fixture)
2+
//!
3+
//! On every event reads `std::time::SystemTime::now()` and logs the wall
4+
//! time as whole seconds since the Unix epoch. Under `wasm32-wasip2` that
5+
//! read routes to `wasi:clocks/wall-clock`, which the supervisor
6+
//! virtualizes per store, so a test that boots this fixture under a pinned
7+
//! clock override can assert from the log line that the guest observed the
8+
//! overridden time rather than the ambient host clock.
9+
//!
10+
//! Not a production module. Lives under `modules/fixtures/` so it is
11+
//! obviously test-only and never gets loaded by the testnet configs.
12+
13+
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
14+
#![allow(clippy::too_many_arguments)]
15+
16+
use std::time::{SystemTime, UNIX_EPOCH};
17+
18+
wit_bindgen::generate!({
19+
path: [
20+
"../../../wit/nexum-value-flow",
21+
"../../../wit/nexum-intent",
22+
"../../../wit/nexum-host",
23+
],
24+
world: "nexum:host/event-module",
25+
generate_all,
26+
});
27+
28+
use nexum::host::{logging, types};
29+
30+
struct ClockReader;
31+
32+
impl Guest for ClockReader {
33+
fn init(_config: Vec<(String, String)>) -> Result<(), Fault> {
34+
// Minimal SDK-free fixture: no tracing subscriber is installed,
35+
// so log through the raw host binding directly.
36+
logging::log(logging::Level::Info, "clock-reader init");
37+
Ok(())
38+
}
39+
40+
fn on_event(_event: types::Event) -> Result<(), Fault> {
41+
// Whole seconds since the epoch is parseable and stable: the
42+
// override pins wall time to an exact instant, so the guest reads
43+
// that instant back rather than the ambient host clock.
44+
let secs = SystemTime::now()
45+
.duration_since(UNIX_EPOCH)
46+
.map(|d| d.as_secs())
47+
.unwrap_or(0);
48+
logging::log(logging::Level::Info, &format!("clock wall {secs}"));
49+
Ok(())
50+
}
51+
}
52+
53+
export!(ClockReader);

0 commit comments

Comments
 (0)