Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/module-durable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ jobs:
- name: Check all Gateway bindings
run: scripts/check-gateway-bindings.py

# Building the workspace Module once keeps the module-path publishers off a cold compile, and
# a build failure reports as a build failure rather than as a publish timeout. The typed taxi
# test builds and privately copies the exact Wasm artifact it inspects.
# Build failures report before any fixture starts. Each test process asks Cargo for the
# current artifact, caches its bytes, and publishes private copies through --bin-path.
- name: Build the module wasm
run: cargo build --release --target wasm32-unknown-unknown -p lyracore-module --features=debug_reducers

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ env_logger = "0.11"
dhat = { version = "0.3", optional = true }

[dev-dependencies]
serde_json = "1"
# Work-item 220 (binding schema-parity test, gateway/tests/schema_parity.rs): the module crate
# compiles natively (its own test suite proves it — no wasm-only dep), so a dev-only dependency
# lets the test compare module `#[table]` structs against the hand-maintained gateway bindings
Expand Down
28 changes: 28 additions & 0 deletions module/tests/fixture_publish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
mod support;

use std::sync::Barrier;

use support::Standalone;

#[test]
#[ignore = "requires the SpacetimeDB 2.7.1 CLI and Wasm toolchain"]
fn concurrent_fixtures_publish_complete_modules() {
let fixtures: Vec<_> = (0..4)
.map(|index| Standalone::start(&format!("concurrent-publish-{index}")))
.collect();
let ready = Barrier::new(fixtures.len());
std::thread::scope(|threads| {
for (index, mut standalone) in fixtures.into_iter().enumerate() {
let ready = &ready;
threads.spawn(move || {
ready.wait();
if index == 0 {
standalone.publish_module_anonymous();
} else {
standalone.publish_module();
}
standalone.assert_sql("SELECT COUNT(*) AS count FROM game_item_template");
});
}
});
}
45 changes: 18 additions & 27 deletions module/tests/support/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
mod module_wasm;

#[allow(unused_imports)] // Some integration targets do not publish a Module.
pub use module_wasm::module_bytes;

use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs::{self, OpenOptions};
Expand Down Expand Up @@ -121,40 +126,26 @@ impl Standalone {
.expect("private Owner Token is missing")
}

#[allow(dead_code)] // Only the integration tests that publish a module use this.
#[allow(dead_code)] // Only the integration tests that publish a Module use this.
pub fn publish_module(&mut self) {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let module_dir = workspace.join("module");
self.publish(
&[
"--module-path",
module_dir.to_str().unwrap(),
"--build-options=--features=debug_reducers",
],
&[],
);
self.publish_module_bytes(module_bytes());
}

/// Copy built Wasm bytes into this standalone's private directory and publish that copy.
#[allow(dead_code)] // Used by tests that build their own Wasm artifact.
/// Copy built Wasm bytes into this Standalone's private directory and publish that copy.
#[allow(dead_code)] // Used by tests that inspect the same artifact they publish.
pub fn publish_module_bytes(&mut self, wasm: &[u8]) {
let path = self.data_dir.join("published-module.wasm");
fs::write(&path, wasm).expect("failed to copy private Wasm artifact");
self.publish(&["--bin-path", path.to_str().unwrap()], &[]);
self.publish_bytes(wasm, &[]);
}

#[allow(dead_code)] // Used when a developer's cached token is not valid for an isolated server.
#[allow(dead_code)] // Used by tests that publish without an Owner Token.
pub fn publish_module_anonymous(&mut self) {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let module_dir = workspace.join("module");
self.publish(
&[
"--module-path",
module_dir.to_str().unwrap(),
"--build-options=--features=debug_reducers",
],
&["--anonymous"],
);
self.publish_bytes(module_bytes(), &["--anonymous"]);
}

fn publish_bytes(&mut self, wasm: &[u8], extra: &[&str]) {
let path = self.data_dir.join("published-module.wasm");
fs::write(&path, wasm).expect("failed to copy private Wasm artifact");
self.publish(&["--bin-path", path.to_str().unwrap()], extra);
}

pub fn call(&self, reducer: &str, args: &[&str]) -> Output {
Expand Down
54 changes: 54 additions & 0 deletions module/tests/support/module_wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::path::PathBuf;
use std::process::Command;
use std::sync::OnceLock;

/// Build once per test process and retain immutable bytes. Publishers copy these into their own
/// directories so concurrent fixtures never share the CLI optimizer's output file.
pub fn module_bytes() -> &'static [u8] {
static WASM: OnceLock<Vec<u8>> = OnceLock::new();
WASM.get_or_init(build_module_bytes)
}

fn build_module_bytes() -> Vec<u8> {
let module_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = module_dir.parent().unwrap();
let output = Command::new("cargo")
.current_dir(workspace)
.args([
"build",
"--locked",
"--release",
"--target",
"wasm32-unknown-unknown",
"-p",
"lyracore-module",
"--features=debug_reducers",
"--message-format=json-render-diagnostics",
])
.output()
.expect("failed to run the test Module build");
assert!(
output.status.success(),
"the test Module build failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);

let wasm = String::from_utf8(output.stdout)
.expect("Cargo artifact output was not UTF-8")
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter(|message| message["reason"] == "compiler-artifact")
.filter(|message| message["target"]["name"] == "lyracore_module")
.filter_map(|message| message["filenames"].as_array().cloned())
.flatten()
.filter_map(|filename| filename.as_str().map(PathBuf::from))
.find(|path| {
path.extension()
.is_some_and(|extension| extension == "wasm")
})
.expect("Cargo did not report the built Module Wasm artifact");
let bytes = std::fs::read(&wasm).expect("the test Module output is missing");
assert!(bytes.starts_with(b"\0asm"), "the built module is not Wasm");
bytes
}
51 changes: 3 additions & 48 deletions module/tests/typed_taxi_contracts.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod support;

use std::path::PathBuf;
use std::process::{Command, Output};
use std::process::Output;

use support::Standalone;

Expand All @@ -11,9 +10,9 @@ const MISSING_ACTOR: &str = "999999";
#[test]
#[ignore = "requires the SpacetimeDB 2.7.1 CLI and Wasm toolchain"]
fn gateway_taxi_gates_keep_refusals_typed_and_invariants_fatal() {
let wasm = build_module_bytes();
let wasm = support::module_bytes();
let mut standalone = Standalone::start("typed-taxi-contracts");
standalone.publish_module_bytes(&wasm);
standalone.publish_module_bytes(wasm);
assert_loot_boundary_failure(&standalone, ACTOR, "loot:boundary_operator_rejected");
standalone.assert_call("claim_operator", &[]);
standalone.assert_call("debug_spawn_player_entity", &[ACTOR]);
Expand Down Expand Up @@ -189,47 +188,3 @@ fn failed_text(reducer: &str, output: Output) -> String {
);
text
}

fn build_module_bytes() -> Vec<u8> {
let module_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = module_dir.parent().unwrap();
let output = Command::new("cargo")
.current_dir(workspace)
.args([
"build",
"--locked",
"--release",
"--target",
"wasm32-unknown-unknown",
"-p",
"lyracore-module",
"--features=debug_reducers",
"--message-format=json-render-diagnostics",
])
.output()
.expect("failed to run the Wasm preflight build");
assert!(
output.status.success(),
"the Wasm preflight build failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);

let wasm = String::from_utf8(output.stdout)
.expect("Cargo artifact output was not UTF-8")
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.filter(|message| message["reason"] == "compiler-artifact")
.filter(|message| message["target"]["name"] == "lyracore_module")
.filter_map(|message| message["filenames"].as_array().cloned())
.flatten()
.filter_map(|filename| filename.as_str().map(PathBuf::from))
.find(|path| {
path.extension()
.is_some_and(|extension| extension == "wasm")
})
.expect("Cargo did not report the built Module Wasm artifact");
let bytes = std::fs::read(&wasm).expect("the Wasm preflight output is missing");
assert!(bytes.starts_with(b"\0asm"), "the built module is not Wasm");
bytes
}