From f8e67f89d78df6636ff5b489460c35081d186273 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 10:54:33 -0700 Subject: [PATCH 01/36] [bfops/cargo-test-all]: cargo test covers all --- Cargo.lock | 35 ++ Cargo.toml | 5 + .../run_type_isolation_test.sh | 11 +- crates/csharp-tests/Cargo.toml | 18 + crates/csharp-tests/tests/csharp.rs | 161 +++++++++ crates/guard/src/lib.rs | 31 +- crates/language-test-support/Cargo.toml | 15 + crates/language-test-support/src/lib.rs | 308 ++++++++++++++++++ crates/typescript-tests/Cargo.toml | 18 + crates/typescript-tests/tests/typescript.rs | 67 ++++ .../regression-tests/client/Program.cs | 2 +- .../procedure-client/Program.cs | 2 +- .../republishing/client/Program.cs | 2 +- sdks/csharp/tools~/run-regression-tests.sh | 13 +- 14 files changed, 668 insertions(+), 20 deletions(-) create mode 100644 crates/csharp-tests/Cargo.toml create mode 100644 crates/csharp-tests/tests/csharp.rs create mode 100644 crates/language-test-support/Cargo.toml create mode 100644 crates/language-test-support/src/lib.rs create mode 100644 crates/typescript-tests/Cargo.toml create mode 100644 crates/typescript-tests/tests/typescript.rs diff --git a/Cargo.lock b/Cargo.lock index 05eefc4b35a..13a9df396ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8136,6 +8136,22 @@ dependencies = [ "wasmtime-internal-fiber", ] +[[package]] +name = "spacetimedb-cpp-tests" +version = "2.1.0" +dependencies = [ + "anyhow", + "spacetimedb-language-test-support", +] + +[[package]] +name = "spacetimedb-csharp-tests" +version = "2.1.0" +dependencies = [ + "anyhow", + "spacetimedb-language-test-support", +] + [[package]] name = "spacetimedb-data-structures" version = "2.1.0" @@ -8296,6 +8312,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "spacetimedb-language-test-support" +version = "2.1.0" +dependencies = [ + "anyhow", + "quick-xml 0.31.0", + "serde_json", + "spacetimedb-guard", + "tempfile", +] + [[package]] name = "spacetimedb-lib" version = "1.9.0" @@ -8784,6 +8811,14 @@ dependencies = [ "wasmbin", ] +[[package]] +name = "spacetimedb-typescript-tests" +version = "2.1.0" +dependencies = [ + "anyhow", + "spacetimedb-language-test-support", +] + [[package]] name = "spacetimedb-update" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index d61d1f44149..ea1081da249 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,10 @@ members = [ "crates/execution", "crates/expr", "crates/guard", + "crates/language-test-support", + "crates/typescript-tests", + "crates/csharp-tests", + "crates/cpp-tests", "crates/fs-utils", "crates/lib", "crates/metrics", @@ -130,6 +134,7 @@ spacetimedb-durability = { path = "crates/durability", version = "=2.1.0" } spacetimedb-execution = { path = "crates/execution", version = "=2.1.0" } spacetimedb-expr = { path = "crates/expr", version = "=2.1.0" } spacetimedb-guard = { path = "crates/guard", version = "=2.1.0" } +spacetimedb-language-test-support = { path = "crates/language-test-support", version = "=2.1.0" } spacetimedb-lib = { path = "crates/lib", default-features = false, version = "=2.1.0" } spacetimedb-memory-usage = { path = "crates/memory-usage", version = "=2.1.0", default-features = false } spacetimedb-metrics = { path = "crates/metrics", version = "=2.1.0" } diff --git a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh index 8fabddb4c4c..4b1d8c7dc37 100644 --- a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh +++ b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh @@ -22,6 +22,7 @@ detect_emcmake_command() { EMCMAKE_CMD=$(detect_emcmake_command) echo "Using emcmake command: $EMCMAKE_CMD" +SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-}" # Parse arguments MAX_PARALLEL=16 @@ -52,7 +53,9 @@ check_server_running() { curl -s http://127.0.0.1:3000/health >/dev/null 2>&1 } -if ! check_server_running; then +if [ -n "$SPACETIMEDB_SERVER_URL" ]; then + echo "Using SpacetimeDB server from SPACETIMEDB_SERVER_URL=$SPACETIMEDB_SERVER_URL" +elif ! check_server_running; then echo "Starting SpacetimeDB server..." nohup spacetime start > "$TMP_DIR/spacetime.log" 2>&1 & @@ -253,7 +256,11 @@ publish_module() { local db_name=$(echo "testmod-${module}" | sed 's/_/-/g') echo " 📤 Publishing $module as $db_name..." local PUBLISH_ERROR_FILE="$TMP_DIR/publish_error_${module}.txt" - timeout 60 spacetime publish --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 + local server_args=() + if [ -n "$SPACETIMEDB_SERVER_URL" ]; then + server_args=(--server "$SPACETIMEDB_SERVER_URL") + fi + timeout 60 spacetime publish "${server_args[@]}" --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 local publish_exit=$? local expected_marker diff --git a/crates/csharp-tests/Cargo.toml b/crates/csharp-tests/Cargo.toml new file mode 100644 index 00000000000..b0d97feacc6 --- /dev/null +++ b/crates/csharp-tests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spacetimedb-csharp-tests" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[[test]] +name = "csharp" +path = "tests/csharp.rs" +harness = false + +[dev-dependencies] +anyhow.workspace = true +spacetimedb-language-test-support.workspace = true + +[lints] +workspace = true diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs new file mode 100644 index 00000000000..95400747ffb --- /dev/null +++ b/crates/csharp-tests/tests/csharp.rs @@ -0,0 +1,161 @@ +use anyhow::{bail, Context, Result}; +use spacetimedb_language_test_support::{ + artifact_dir, parse_trx, print_results, require_tool, run_command, run_command_env, run_command_forward, + HarnessArgs, SpacetimeDbGuard, +}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn main() { + if let Err(err) = run() { + eprintln!("{err:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = HarnessArgs::parse(); + require_tool("dotnet")?; + + let workspace = spacetimedb_language_test_support::workspace_root(); + let out_dir = artifact_dir("csharp")?; + + run_bindings_tests(&workspace, &out_dir, &args)?; + run_sdk_tests(&workspace, &out_dir, &args)?; + if !args.list { + run_regression_tests(&workspace)?; + } + + Ok(()) +} + +fn run_bindings_tests(workspace: &Path, out_dir: &Path, args: &HarnessArgs) -> Result<()> { + let cwd = workspace.join("crates/bindings-csharp"); + run_dotnet_test("csharp bindings", &cwd, out_dir, "bindings.trx", args, &[])?; + Ok(()) +} + +fn run_sdk_tests(workspace: &Path, out_dir: &Path, args: &HarnessArgs) -> Result<()> { + prepare_csharp_sdk_solution(workspace)?; + let cwd = workspace.join("sdks/csharp"); + run_dotnet_test( + "csharp sdk", + &cwd, + out_dir, + "sdk.trx", + args, + &["-warnaserror".to_string(), "--no-restore".to_string()], + )?; + Ok(()) +} + +fn run_dotnet_test( + suite: &str, + cwd: &Path, + out_dir: &Path, + report_name: &str, + args: &HarnessArgs, + extra_args: &[String], +) -> Result<()> { + let report = out_dir.join(report_name); + + if args.list { + let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; + list_args.extend(extra_args.iter().cloned()); + run_command_forward("dotnet", &list_args, cwd)?; + return Ok(()); + } + + let mut test_args = vec![ + "test".to_string(), + "-warnaserror".to_string(), + "--results-directory".to_string(), + out_dir.display().to_string(), + "--logger".to_string(), + format!("trx;LogFileName={report_name}"), + ]; + test_args.extend(extra_args.iter().filter(|arg| arg.as_str() != "-warnaserror").cloned()); + if let Some(filter) = &args.filter { + test_args.push("--filter".to_string()); + test_args.push(filter.clone()); + } + test_args.extend(args.passthrough.iter().cloned()); + + run_command("dotnet", &test_args, cwd)?; + let actual_report = find_trx(&report, cwd).with_context(|| format!("failed to locate TRX report for {suite}"))?; + let results = parse_trx(&actual_report).with_context(|| format!("failed to parse {suite} TRX report"))?; + print_results(suite, &actual_report, &results)?; + Ok(()) +} + +fn find_trx(preferred: &Path, cwd: &Path) -> Result { + if preferred.exists() { + return Ok(preferred.to_path_buf()); + } + let name = preferred + .file_name() + .and_then(|name| name.to_str()) + .context("invalid TRX report name")?; + let test_results = cwd.join("TestResults"); + for entry in fs::read_dir(&test_results).with_context(|| format!("failed to read {}", test_results.display()))? { + let entry = entry?; + let path = entry.path().join(name); + if path.exists() { + return Ok(path); + } + } + bail!("TRX report {} not found", preferred.display()) +} + +fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { + run_command( + "dotnet", + &[ + "pack".to_string(), + "crates/bindings-csharp/BSATN.Runtime".to_string(), + "-c".to_string(), + "Release".to_string(), + ], + workspace, + )?; + run_command( + "dotnet", + &[ + "pack".to_string(), + "crates/bindings-csharp/Runtime".to_string(), + "-c".to_string(), + "Release".to_string(), + ], + workspace, + )?; + run_command( + "bash", + &["./tools~/write-nuget-config.sh".to_string(), "../..".to_string()], + &workspace.join("sdks/csharp"), + )?; + run_command( + "dotnet", + &[ + "restore".to_string(), + "--configfile".to_string(), + "NuGet.Config".to_string(), + "SpacetimeDB.ClientSDK.sln".to_string(), + ], + &workspace.join("sdks/csharp"), + )?; + Ok(()) +} + +fn run_regression_tests(workspace: &Path) -> Result<()> { + require_tool("bash")?; + // The regression module itself still performs an HTTP egress call to + // localhost:3000, so this specific suite needs the fixed listen address. + let guard = SpacetimeDbGuard::spawn_in_temp_data_dir_with_listen_addr("127.0.0.1:3000"); + run_command_env( + "bash", + &["tools~/run-regression-tests.sh".to_string()], + &workspace.join("sdks/csharp"), + &[("SPACETIMEDB_SERVER_URL", guard.host_url.clone())], + )?; + Ok(()) +} diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index af937910a54..c296212bb8b 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -123,7 +123,15 @@ impl SpacetimeDbGuard { let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); let data_dir_path = temp_dir.path().to_path_buf(); - Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir_path, Some(temp_dir)) + Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir_path, Some(temp_dir), "127.0.0.1:0") + } + + /// Start `spacetimedb` in a temporary data directory on a specific listen address. + pub fn spawn_in_temp_data_dir_with_listen_addr(listen_addr: &str) -> Self { + let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); + let data_dir_path = temp_dir.path().to_path_buf(); + + Self::spawn_spacetime_start_with_data_dir(false, None, data_dir_path, Some(temp_dir), listen_addr) } /// Start `spacetimedb` in a temporary data directory via: @@ -132,7 +140,7 @@ impl SpacetimeDbGuard { let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); let data_dir_path = temp_dir.path().to_path_buf(); - Self::spawn_spacetime_start_with_data_dir(true, None, data_dir_path, Some(temp_dir)) + Self::spawn_spacetime_start_with_data_dir(true, None, data_dir_path, Some(temp_dir), "127.0.0.1:0") } /// Start `spacetimedb` with an explicit data directory (for restart scenarios). @@ -140,7 +148,7 @@ impl SpacetimeDbGuard { /// Unlike `spawn_in_temp_data_dir`, this method does not create a temporary directory. /// The caller is responsible for managing the data directory lifetime. pub fn spawn_with_data_dir(data_dir: PathBuf, pg_port: Option) -> Self { - Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir, None) + Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir, None, "127.0.0.1:0") } fn spawn_spacetime_start_with_data_dir( @@ -148,10 +156,11 @@ impl SpacetimeDbGuard { pg_port: Option, data_dir: PathBuf, _data_dir_handle: Option, + listen_addr: &str, ) -> Self { let spawn_id = next_spawn_id(); let (child, logs, host_url, reader_threads) = - Self::spawn_server(&data_dir, pg_port, spawn_id, use_installed_cli); + Self::spawn_server(&data_dir, pg_port, spawn_id, use_installed_cli, listen_addr); SpacetimeDbGuard { child, host_url, @@ -188,8 +197,13 @@ impl SpacetimeDbGuard { sleep(Duration::from_millis(100)); eprintln!("[RESTART-{:03}] Spawning new server", spawn_id); - let (child, logs, host_url, reader_threads) = - Self::spawn_server(&self.data_dir, self.pg_port, spawn_id, self.use_installed_cli); + let (child, logs, host_url, reader_threads) = Self::spawn_server( + &self.data_dir, + self.pg_port, + spawn_id, + self.use_installed_cli, + "127.0.0.1:0", + ); eprintln!( "[RESTART-{:03}] New server ready, pid={}, url={}", spawn_id, @@ -252,6 +266,7 @@ impl SpacetimeDbGuard { pg_port: Option, spawn_id: u64, use_installed_cli: bool, + listen_addr: &str, ) -> (Child, Arc>, String, Vec>) { let cmd = if use_installed_cli { eprintln!("[SPAWN-{:03}] START Using installed CLI", spawn_id); @@ -269,8 +284,6 @@ impl SpacetimeDbGuard { let data_dir_str = data_dir.display().to_string(); let pg_port_str = pg_port.map(|p| p.to_string()); - let address = "127.0.0.1:0".to_string(); - let mut args = vec![ "start", "--jwt-key-dir", @@ -278,7 +291,7 @@ impl SpacetimeDbGuard { "--data-dir", &data_dir_str, "--listen-addr", - &address, + listen_addr, ]; if let Some(ref port) = pg_port_str { args.extend(["--pg-port", port]); diff --git a/crates/language-test-support/Cargo.toml b/crates/language-test-support/Cargo.toml new file mode 100644 index 00000000000..b76c1c889ae --- /dev/null +++ b/crates/language-test-support/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "spacetimedb-language-test-support" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow.workspace = true +quick-xml.workspace = true +serde_json.workspace = true +spacetimedb-guard.workspace = true +tempfile.workspace = true + +[lints] +workspace = true diff --git a/crates/language-test-support/src/lib.rs b/crates/language-test-support/src/lib.rs new file mode 100644 index 00000000000..b5ba503ce08 --- /dev/null +++ b/crates/language-test-support/src/lib.rs @@ -0,0 +1,308 @@ +#![allow(clippy::disallowed_macros)] + +use anyhow::{bail, Context, Result}; +use quick_xml::events::Event; +use quick_xml::Reader; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +pub use spacetimedb_guard::SpacetimeDbGuard; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Outcome { + Passed, + Failed, + Skipped, +} + +#[derive(Clone, Debug)] +pub struct TestCaseResult { + pub name: String, + pub outcome: Outcome, + pub message: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct HarnessArgs { + pub filter: Option, + pub list: bool, + pub passthrough: Vec, +} + +impl HarnessArgs { + pub fn parse() -> Self { + let mut args = env::args().skip(1).peekable(); + let mut parsed = HarnessArgs::default(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "--filter" => { + parsed.filter = args.next(); + } + "--list" | "--list-tests" => { + parsed.list = true; + } + "--" => { + parsed.passthrough.extend(args); + break; + } + other if other.starts_with("--filter=") => { + parsed.filter = Some(other.trim_start_matches("--filter=").to_string()); + } + other if other.starts_with("--") => { + parsed.passthrough.push(other.to_string()); + } + other => { + // Match libtest's common shorthand: `cargo test foo`. + if parsed.filter.is_none() { + parsed.filter = Some(other.to_string()); + } else { + parsed.passthrough.push(other.to_string()); + } + } + } + } + + parsed + } +} + +pub fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("language-test-support should live under /crates") + .to_path_buf() +} + +pub fn target_dir() -> PathBuf { + env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| workspace_root().join("target")) +} + +pub fn artifact_dir(suite: &str) -> Result { + let dir = target_dir().join("language-tests").join(suite); + fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?; + Ok(dir) +} + +pub fn require_tool(tool: &str) -> Result<()> { + if find_on_path(tool).is_some() { + Ok(()) + } else { + bail!("required tool `{tool}` was not found on PATH") + } +} + +fn find_on_path(tool: &str) -> Option { + let path = env::var_os("PATH")?; + let candidates = env::split_paths(&path).flat_map(|dir| executable_candidates(&dir, tool)); + candidates.into_iter().find(|candidate| candidate.is_file()) +} + +fn executable_candidates(dir: &Path, tool: &str) -> Vec { + #[cfg(windows)] + { + let mut candidates = vec![dir.join(tool)]; + if Path::new(tool).extension().is_none() { + let pathext = env::var_os("PATHEXT") + .map(|v| { + env::split_paths(&v) + .filter_map(|p| p.as_os_str().to_str().map(ToOwned::to_owned)) + .collect::>() + }) + .unwrap_or_else(|| vec![".exe".to_string(), ".bat".to_string(), ".cmd".to_string()]); + candidates.extend(pathext.into_iter().map(|ext| dir.join(format!("{tool}{ext}")))); + } + candidates + } + + #[cfg(not(windows))] + { + vec![dir.join(tool)] + } +} + +pub fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { + let output = Command::new(program) + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + + if !output.status.success() { + bail!( + "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + shell_line(program, args), + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output) +} + +pub fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { + let output = run_command(program, args, cwd)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + Ok(()) +} + +pub fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { + let output = Command::new(program) + .args(args) + .current_dir(cwd) + .envs(envs.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)))) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + + if !output.status.success() { + bail!( + "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + shell_line(program, args), + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + Ok(output) +} + +pub fn shell_line(program: &str, args: &[String]) -> String { + std::iter::once(program.to_string()) + .chain(args.iter().cloned()) + .collect::>() + .join(" ") +} + +pub fn parse_trx(path: &Path) -> Result> { + parse_xml_results(path, XmlKind::Trx) +} + +pub fn parse_junit(path: &Path) -> Result> { + parse_xml_results(path, XmlKind::Junit) +} + +enum XmlKind { + Trx, + Junit, +} + +fn parse_xml_results(path: &Path, kind: XmlKind) -> Result> { + let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; + reader.trim_text(true); + + let mut buf = Vec::new(); + let mut results = Vec::new(); + let mut current_junit: Option = None; + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Empty(e)) | Ok(Event::Start(e)) => match (e.name().as_ref(), &kind) { + (b"UnitTestResult", XmlKind::Trx) => { + let name = attr(&e, b"testName")?.unwrap_or_else(|| "".to_string()); + let outcome = match attr(&e, b"outcome")?.as_deref() { + Some("Passed") => Outcome::Passed, + Some("NotExecuted") => Outcome::Skipped, + Some("Failed") => Outcome::Failed, + _ => Outcome::Failed, + }; + results.push(TestCaseResult { + name, + outcome, + message: None, + }); + } + (b"testcase", XmlKind::Junit) => { + let name = attr(&e, b"name")?.unwrap_or_else(|| "".to_string()); + let class = attr(&e, b"classname")?; + let name = class.map(|class| format!("{class}::{name}")).unwrap_or(name); + current_junit = Some(TestCaseResult { + name, + outcome: Outcome::Passed, + message: None, + }); + if e.is_empty() + && let Some(case) = current_junit.take() + { + results.push(case); + } + } + (b"failure" | b"error", XmlKind::Junit) => { + if let Some(case) = current_junit.as_mut() { + case.outcome = Outcome::Failed; + case.message = attr(&e, b"message")?; + } + } + (b"skipped", XmlKind::Junit) => { + if let Some(case) = current_junit.as_mut() { + case.outcome = Outcome::Skipped; + case.message = attr(&e, b"message")?; + } + } + _ => {} + }, + Ok(Event::End(e)) if matches!((&kind, e.name().as_ref()), (XmlKind::Junit, b"testcase")) => { + if let Some(case) = current_junit.take() { + results.push(case); + } + } + Ok(Event::Eof) => break, + Err(err) => bail!("failed to parse {}: {err}", path.display()), + _ => {} + } + buf.clear(); + } + + Ok(results) +} + +fn attr(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Result> { + for attr in e.attributes() { + let attr = attr?; + if attr.key.as_ref() == key { + return Ok(Some(String::from_utf8_lossy(attr.value.as_ref()).to_string())); + } + } + Ok(None) +} + +pub fn print_results(suite: &str, report_path: &Path, results: &[TestCaseResult]) -> Result<()> { + let passed = results.iter().filter(|r| r.outcome == Outcome::Passed).count(); + let failed = results.iter().filter(|r| r.outcome == Outcome::Failed).count(); + let skipped = results.iter().filter(|r| r.outcome == Outcome::Skipped).count(); + + println!( + "{suite}: parsed {} test results from {}", + results.len(), + report_path.display() + ); + for result in results { + let status = match result.outcome { + Outcome::Passed => "ok", + Outcome::Failed => "FAILED", + Outcome::Skipped => "ignored", + }; + println!("{status:7} {}", result.name); + if let Some(message) = &result.message + && !message.is_empty() + { + println!(" {message}"); + } + } + println!("{suite}: {passed} passed; {failed} failed; {skipped} skipped"); + + if failed > 0 { + bail!("{suite}: {failed} native tests failed"); + } + Ok(()) +} diff --git a/crates/typescript-tests/Cargo.toml b/crates/typescript-tests/Cargo.toml new file mode 100644 index 00000000000..2994449d99c --- /dev/null +++ b/crates/typescript-tests/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spacetimedb-typescript-tests" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[[test]] +name = "typescript" +path = "tests/typescript.rs" +harness = false + +[dev-dependencies] +anyhow.workspace = true +spacetimedb-language-test-support.workspace = true + +[lints] +workspace = true diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs new file mode 100644 index 00000000000..01a367158ae --- /dev/null +++ b/crates/typescript-tests/tests/typescript.rs @@ -0,0 +1,67 @@ +use anyhow::{Context, Result}; +use spacetimedb_language_test_support::{ + artifact_dir, parse_junit, print_results, require_tool, run_command, run_command_forward, HarnessArgs, +}; + +fn main() { + if let Err(err) = run() { + eprintln!("{err:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = HarnessArgs::parse(); + require_tool("pnpm")?; + + let workspace = spacetimedb_language_test_support::workspace_root(); + let cwd = workspace.join("crates/bindings-typescript"); + let out_dir = artifact_dir("typescript")?; + let report = out_dir.join("vitest.junit.xml"); + + if args.list { + let mut cmd = vec!["vitest".to_string(), "list".to_string()]; + if let Some(filter) = args.filter { + cmd.push(filter); + } + run_command_forward("pnpm", &cmd, &cwd)?; + return Ok(()); + } + + run_command("pnpm", &["build".to_string()], &cwd)?; + + let mut test_args = vec![ + "vitest".to_string(), + "run".to_string(), + "--reporter=default".to_string(), + "--reporter=junit".to_string(), + format!("--outputFile={}", report.display()), + ]; + if let Some(filter) = args.filter { + test_args.push("-t".to_string()); + test_args.push(filter); + } + test_args.extend(args.passthrough); + run_command("pnpm", &test_args, &cwd)?; + + let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; + print_results("typescript", &report, &results)?; + + let typecheck_report = out_dir.join("vitest-typecheck.junit.xml"); + let typecheck_args = vec![ + "vitest".to_string(), + "typecheck".to_string(), + "--run".to_string(), + "--reporter=default".to_string(), + "--reporter=junit".to_string(), + format!("--outputFile={}", typecheck_report.display()), + ]; + run_command("pnpm", &typecheck_args, &cwd)?; + if typecheck_report.exists() { + let results = parse_junit(&typecheck_report) + .with_context(|| "failed to parse TypeScript Vitest typecheck JUnit report")?; + print_results("typescript typecheck", &typecheck_report, &results)?; + } + + Ok(()) +} diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index 4e254fed3c7..c3b88d1b05a 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -12,7 +12,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "btree-repro"; const string THROW_ERROR_MESSAGE = "this is an error"; const uint UPDATED_WHERE_TEST_VALUE = 42; diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs index 8997f596354..2345af3af15 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs @@ -9,7 +9,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "procedure-tests"; uint waiting = 0; diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs index e8493cee87f..41fc80bb8b3 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs @@ -9,7 +9,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "republish-test"; uint waiting = 0; diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index 847409c9c32..168eeab668a 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -7,6 +7,7 @@ set -ueo pipefail SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" STDB_PATH="$SDK_PATH/../.." +SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-local}" # Regenerate Bindings "$SDK_PATH/tools~/gen-regression-tests.sh" @@ -15,13 +16,13 @@ STDB_PATH="$SDK_PATH/../.." cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" # Publish module for btree test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$SDK_PATH/examples~/regression-tests/server" btree-repro +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/server" btree-repro # Publish module for republishing module test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$SDK_PATH/examples~/regression-tests/republishing/server-initial" republish-test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local republish-test insert 1 -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish --server local -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local republish-test insert 2 +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/republishing/server-initial" republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server "$SPACETIMEDB_SERVER_URL" republish-test insert 1 +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server "$SPACETIMEDB_SERVER_URL" republish-test insert 2 echo "Cleanup obj~ folders generated in $SDK_PATH/examples~/regression-tests/procedure-client" # There is a bug in the code generator that creates obj~ folders in the output directory using a Rust project. @@ -29,7 +30,7 @@ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client"/*/obj~ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings"/*/obj~ # Publish module for procedure tests -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests # Run client for btree test cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug From 7221336e9c57f20c35ba7921dfe8527aa1fd1a98 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 11:08:14 -0700 Subject: [PATCH 02/36] [bfops/cargo-ci-test]: cargo ci --- .github/workflows/ci.yml | 58 +------------------ .../run_type_isolation_test.sh | 11 +--- tools/ci/src/main.rs | 56 +++++++++++++++++- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69bfe7f0e76..9064fa6b0b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -760,32 +760,6 @@ jobs: with: global-json-file: global.json - - name: Override NuGet packages - run: | - dotnet pack crates/bindings-csharp/BSATN.Runtime - dotnet pack crates/bindings-csharp/Runtime - - # Write out the nuget config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository - # to be aware of the local versions of the `bindings-csharp` packages in SpacetimeDB, and use them if - # available. Otherwise, `spacetimedb-csharp-sdk` will use the NuGet versions of the packages. - # This means that (if version numbers match) we will test the local versions of the C# packages, even - # if they're not pushed to NuGet. - # See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file. - cd sdks/csharp - ./tools~/write-nuget-config.sh ../.. - - - name: Restore .NET solution - working-directory: sdks/csharp - run: dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln - - - name: Run .NET tests - working-directory: sdks/csharp - run: dotnet test -warnaserror --no-restore - - - name: Verify C# formatting - working-directory: sdks/csharp - run: dotnet format --no-restore --verify-no-changes SpacetimeDB.ClientSDK.sln - - name: Install Rust toolchain uses: dsherret/rust-toolchain-file@v1 - name: Set default rust toolchain @@ -820,23 +794,8 @@ jobs: cargo build --release -p v8 fi - - name: Install SpacetimeDB CLI from the local checkout - run: | - export CARGO_HOME="$HOME/.cargo" - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - cargo install --force --path crates/cli --locked --message-format=short - cargo install --force --path crates/standalone --features allow_loopback_http_for_tests --locked --message-format=short - # Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules). - ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime - - - name: Check quickstart-chat bindings are up to date - working-directory: sdks/csharp - run: | - bash tools~/gen-quickstart.sh - "${GITHUB_WORKSPACE}"/tools/check-diff.sh examples~/quickstart-chat || { - echo 'Error: quickstart-chat bindings have changed. Please run `sdks/csharp/tools~/gen-quickstart.sh`.' - exit 1 - } + - name: Run C# tests + run: cargo ci csharp-tests # TODO: Re-enable this once csharp is using the v2 ws api. # - name: Check client-api bindings are up to date @@ -848,19 +807,6 @@ jobs: # exit 1 # } - - name: Start SpacetimeDB - run: | - spacetime start & - disown - - - name: Run regression tests - run: | - bash sdks/csharp/tools~/run-regression-tests.sh - tools/check-diff.sh sdks/csharp/examples~/regression-tests || { - echo 'Error: Bindings are dirty. Please run `sdks/csharp/tools~/gen-regression-tests.sh`.' - exit 1 - } - internal-tests: name: Internal Tests needs: [lints] diff --git a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh index 4b1d8c7dc37..8fabddb4c4c 100644 --- a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh +++ b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh @@ -22,7 +22,6 @@ detect_emcmake_command() { EMCMAKE_CMD=$(detect_emcmake_command) echo "Using emcmake command: $EMCMAKE_CMD" -SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-}" # Parse arguments MAX_PARALLEL=16 @@ -53,9 +52,7 @@ check_server_running() { curl -s http://127.0.0.1:3000/health >/dev/null 2>&1 } -if [ -n "$SPACETIMEDB_SERVER_URL" ]; then - echo "Using SpacetimeDB server from SPACETIMEDB_SERVER_URL=$SPACETIMEDB_SERVER_URL" -elif ! check_server_running; then +if ! check_server_running; then echo "Starting SpacetimeDB server..." nohup spacetime start > "$TMP_DIR/spacetime.log" 2>&1 & @@ -256,11 +253,7 @@ publish_module() { local db_name=$(echo "testmod-${module}" | sed 's/_/-/g') echo " 📤 Publishing $module as $db_name..." local PUBLISH_ERROR_FILE="$TMP_DIR/publish_error_${module}.txt" - local server_args=() - if [ -n "$SPACETIMEDB_SERVER_URL" ]; then - server_args=(--server "$SPACETIMEDB_SERVER_URL") - fi - timeout 60 spacetime publish "${server_args[@]}" --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 + timeout 60 spacetime publish --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 local publish_exit=$? local expected_marker diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 3c31c366324..843ddf6009a 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -276,6 +276,8 @@ enum CiCmd { PublishChecks, /// Runs TypeScript workspace tests and template build checks. TypescriptTest, + /// Runs C# tests through the Cargo language-test harness. + CsharpTests, /// Builds the docs site. Docs, } @@ -415,8 +417,15 @@ fn run_publish_checks() -> Result<()> { } fn run_typescript_tests() -> Result<()> { - cmd!("pnpm", "build").dir("crates/bindings-typescript").run()?; - cmd!("pnpm", "test").dir("crates/bindings-typescript").run()?; + cmd!( + "cargo", + "test", + "-p", + "spacetimedb-typescript-tests", + "--test", + "typescript" + ) + .run()?; cmd!("pnpm", "generate").dir("templates/chat-react-ts").run()?; let diff_status = cmd!( "bash", @@ -437,6 +446,41 @@ fn run_typescript_tests() -> Result<()> { Ok(()) } +fn run_csharp_tests() -> Result<()> { + cmd!( + "cargo", + "build", + "--release", + "-p", + "spacetimedb-cli", + "-p", + "spacetimedb-standalone", + "--features", + "spacetimedb-standalone/allow_loopback_http_for_tests" + ) + .run()?; + cmd!("cargo", "test", "-p", "spacetimedb-csharp-tests", "--test", "csharp").run()?; + cmd!( + "dotnet", + "format", + "--no-restore", + "--verify-no-changes", + "SpacetimeDB.ClientSDK.sln" + ) + .dir("sdks/csharp") + .run()?; + cmd!("bash", "tools~/gen-quickstart.sh").dir("sdks/csharp").run()?; + let diff_status = cmd!("bash", "tools/check-diff.sh", "sdks/csharp/examples~/quickstart-chat").run()?; + if !diff_status.status.success() { + bail!("quickstart-chat bindings have changed. Please run `sdks/csharp/tools~/gen-quickstart.sh`."); + } + let diff_status = cmd!("bash", "tools/check-diff.sh", "sdks/csharp/examples~/regression-tests").run()?; + if !diff_status.status.success() { + bail!("Bindings are dirty. Please run `sdks/csharp/tools~/gen-regression-tests.sh`."); + } + Ok(()) +} + fn run_docs_build() -> Result<()> { cmd!("pnpm", "install").dir("docs").run()?; cmd!("pnpm", "build").dir("docs").run()?; @@ -464,6 +508,10 @@ fn main() -> Result<()> { "spacetimedb-smoketests", "--exclude", "spacetimedb-sdk", + "--exclude", + "spacetimedb-typescript-tests", + "--exclude", + "spacetimedb-csharp-tests", "--", "--test-threads=2", "--skip", @@ -711,6 +759,10 @@ fn main() -> Result<()> { run_typescript_tests()?; } + Some(CiCmd::CsharpTests) => { + run_csharp_tests()?; + } + Some(CiCmd::Docs) => { run_docs_build()?; } From 1a026240d0af2b219fc808c0c93b7b1af24d5855 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 11:10:26 -0700 Subject: [PATCH 03/36] [bfops/cargo-test-all]: remove cpp stuff --- Cargo.lock | 8 -------- Cargo.toml | 1 - 2 files changed, 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13a9df396ab..b1651d7955f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8136,14 +8136,6 @@ dependencies = [ "wasmtime-internal-fiber", ] -[[package]] -name = "spacetimedb-cpp-tests" -version = "2.1.0" -dependencies = [ - "anyhow", - "spacetimedb-language-test-support", -] - [[package]] name = "spacetimedb-csharp-tests" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index ea1081da249..d9860e6830d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ members = [ "crates/language-test-support", "crates/typescript-tests", "crates/csharp-tests", - "crates/cpp-tests", "crates/fs-utils", "crates/lib", "crates/metrics", From 6f8c15acd142f33bd6082f509961d199a9f88144 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 11:35:01 -0700 Subject: [PATCH 04/36] [bfops/cargo-test-all]: lints --- crates/csharp-tests/tests/csharp.rs | 2 ++ crates/typescript-tests/tests/typescript.rs | 2 ++ tools/ci/README.md | 4 ++-- tools/ci/src/main.rs | 6 ++++++ tools/ci/src/smoketest.rs | 2 +- tools/run-all-tests.sh | 6 ++++-- 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 95400747ffb..b1d8a39be87 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + use anyhow::{bail, Context, Result}; use spacetimedb_language_test_support::{ artifact_dir, parse_trx, print_results, require_tool, run_command, run_command_env, run_command_forward, diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 01a367158ae..a27667c34c3 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + use anyhow::{Context, Result}; use spacetimedb_language_test_support::{ artifact_dir, parse_junit, print_results, require_tool, run_command, run_command_forward, HarnessArgs, diff --git a/tools/ci/README.md b/tools/ci/README.md index dbe452243f0..888148030b8 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -110,7 +110,7 @@ When specified, tests will connect to the given URL instead of starting local se Only build binaries without running tests -Use this before running `cargo test --all` to ensure binaries are built. +Use this before running broad workspace tests to ensure binaries are built. **Usage:** ```bash @@ -246,4 +246,4 @@ This document is auto-generated by running: ```bash cargo ci self-docs -``` \ No newline at end of file +``` diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 3c31c366324..bc6528981bb 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -464,6 +464,12 @@ fn main() -> Result<()> { "spacetimedb-smoketests", "--exclude", "spacetimedb-sdk", + "--exclude", + "spacetimedb-language-test-support", + "--exclude", + "spacetimedb-typescript-tests", + "--exclude", + "spacetimedb-csharp-tests", "--", "--test-threads=2", "--skip", diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 456c13caa32..446d184b7c1 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -38,7 +38,7 @@ pub struct SmoketestsArgs { enum SmoketestCmd { /// Only build binaries without running tests /// - /// Use this before running `cargo test --all` to ensure binaries are built. + /// Use this before running broad workspace tests to ensure binaries are built. Prepare, CheckModList, } diff --git a/tools/run-all-tests.sh b/tools/run-all-tests.sh index a2d0dbf9958..10feab8d94b 100755 --- a/tools/run-all-tests.sh +++ b/tools/run-all-tests.sh @@ -11,7 +11,10 @@ cd "$stdb_root" tools/clippy.sh -cargo test --all +cargo test --all \ + --exclude spacetimedb-language-test-support \ + --exclude spacetimedb-typescript-tests \ + --exclude spacetimedb-csharp-tests if which python3 >/dev/null ; then python3 -m smoketests @@ -26,4 +29,3 @@ if which dotnet >/dev/null ; then else echo "Can't find dotnet, not running smoketests" fi - From f8b89fb1185b765509a80337ea0904341d35aabb Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 14:56:20 -0700 Subject: [PATCH 05/36] [bfops/cargo-test-all]: update docs --- tools/ci/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/README.md b/tools/ci/README.md index 888148030b8..693631943c1 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -246,4 +246,4 @@ This document is auto-generated by running: ```bash cargo ci self-docs -``` +``` \ No newline at end of file From ed4859aab78e468bab882b9a8770db34a3b616fc Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:07:22 -0700 Subject: [PATCH 06/36] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index bc6528981bb..632ca7d83b9 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -465,8 +465,6 @@ fn main() -> Result<()> { "--exclude", "spacetimedb-sdk", "--exclude", - "spacetimedb-language-test-support", - "--exclude", "spacetimedb-typescript-tests", "--exclude", "spacetimedb-csharp-tests", From 61898fa0bb096dbb35516d8ef3df051e31d44ccc Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Thu, 30 Apr 2026 15:07:37 -0700 Subject: [PATCH 07/36] [bfops/cargo-test-all]: revert --- .../type-isolation-test/run_type_isolation_test.sh | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh index 4b1d8c7dc37..8fabddb4c4c 100644 --- a/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh +++ b/crates/bindings-cpp/tests/type-isolation-test/run_type_isolation_test.sh @@ -22,7 +22,6 @@ detect_emcmake_command() { EMCMAKE_CMD=$(detect_emcmake_command) echo "Using emcmake command: $EMCMAKE_CMD" -SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-}" # Parse arguments MAX_PARALLEL=16 @@ -53,9 +52,7 @@ check_server_running() { curl -s http://127.0.0.1:3000/health >/dev/null 2>&1 } -if [ -n "$SPACETIMEDB_SERVER_URL" ]; then - echo "Using SpacetimeDB server from SPACETIMEDB_SERVER_URL=$SPACETIMEDB_SERVER_URL" -elif ! check_server_running; then +if ! check_server_running; then echo "Starting SpacetimeDB server..." nohup spacetime start > "$TMP_DIR/spacetime.log" 2>&1 & @@ -256,11 +253,7 @@ publish_module() { local db_name=$(echo "testmod-${module}" | sed 's/_/-/g') echo " 📤 Publishing $module as $db_name..." local PUBLISH_ERROR_FILE="$TMP_DIR/publish_error_${module}.txt" - local server_args=() - if [ -n "$SPACETIMEDB_SERVER_URL" ]; then - server_args=(--server "$SPACETIMEDB_SERVER_URL") - fi - timeout 60 spacetime publish "${server_args[@]}" --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 + timeout 60 spacetime publish --bin-path "$wasm" -c "$db_name" -y >"$PUBLISH_ERROR_FILE" 2>&1 local publish_exit=$? local expected_marker From 0c0e04d8ab1bd016c9df87a4f217f8b3cebe4f51 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Fri, 1 May 2026 10:16:18 -0700 Subject: [PATCH 08/36] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/run-all-tests.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/run-all-tests.sh b/tools/run-all-tests.sh index 10feab8d94b..3cae2186042 100755 --- a/tools/run-all-tests.sh +++ b/tools/run-all-tests.sh @@ -12,7 +12,6 @@ cd "$stdb_root" tools/clippy.sh cargo test --all \ - --exclude spacetimedb-language-test-support \ --exclude spacetimedb-typescript-tests \ --exclude spacetimedb-csharp-tests From d98137b64e0f23aa3b9e045eb55a9b3d78829c1d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 10:18:48 -0700 Subject: [PATCH 09/36] [bfops/cargo-test-all]: revert --- tools/run-all-tests.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/run-all-tests.sh b/tools/run-all-tests.sh index 3cae2186042..a2d0dbf9958 100755 --- a/tools/run-all-tests.sh +++ b/tools/run-all-tests.sh @@ -11,9 +11,7 @@ cd "$stdb_root" tools/clippy.sh -cargo test --all \ - --exclude spacetimedb-typescript-tests \ - --exclude spacetimedb-csharp-tests +cargo test --all if which python3 >/dev/null ; then python3 -m smoketests @@ -28,3 +26,4 @@ if which dotnet >/dev/null ; then else echo "Can't find dotnet, not running smoketests" fi + From 170ef8fa18c2bf98f3fee5928be65c75b0649b7e Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 11:18:50 -0700 Subject: [PATCH 10/36] [bfops/flexible-csharp]: CI - C# test scripts accept SPACETIMEDB_SERVER_URL --- .../examples~/regression-tests/client/Program.cs | 2 +- .../regression-tests/procedure-client/Program.cs | 2 +- .../regression-tests/republishing/client/Program.cs | 2 +- sdks/csharp/tools~/run-regression-tests.sh | 13 +++++++------ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index 4e254fed3c7..c3b88d1b05a 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -12,7 +12,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "btree-repro"; const string THROW_ERROR_MESSAGE = "this is an error"; const uint UPDATED_WHERE_TEST_VALUE = 42; diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs index 8997f596354..2345af3af15 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/procedure-client/Program.cs @@ -9,7 +9,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "procedure-tests"; uint waiting = 0; diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs index e8493cee87f..41fc80bb8b3 100644 --- a/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/republishing/client/Program.cs @@ -9,7 +9,7 @@ using SpacetimeDB; using SpacetimeDB.Types; -const string HOST = "http://localhost:3000"; +string HOST = Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? "http://localhost:3000"; const string DBNAME = "republish-test"; uint waiting = 0; diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index 847409c9c32..168eeab668a 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -7,6 +7,7 @@ set -ueo pipefail SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" STDB_PATH="$SDK_PATH/../.." +SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-local}" # Regenerate Bindings "$SDK_PATH/tools~/gen-regression-tests.sh" @@ -15,13 +16,13 @@ STDB_PATH="$SDK_PATH/../.." cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" # Publish module for btree test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$SDK_PATH/examples~/regression-tests/server" btree-repro +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/server" btree-repro # Publish module for republishing module test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$SDK_PATH/examples~/regression-tests/republishing/server-initial" republish-test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local republish-test insert 1 -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish --server local -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server local republish-test insert 2 +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/republishing/server-initial" republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server "$SPACETIMEDB_SERVER_URL" republish-test insert 1 +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish --server "$SPACETIMEDB_SERVER_URL" -p "$SDK_PATH/examples~/regression-tests/republishing/server-republish" --break-clients republish-test +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call --server "$SPACETIMEDB_SERVER_URL" republish-test insert 2 echo "Cleanup obj~ folders generated in $SDK_PATH/examples~/regression-tests/procedure-client" # There is a bug in the code generator that creates obj~ folders in the output directory using a Rust project. @@ -29,7 +30,7 @@ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client"/*/obj~ rm -rf "$SDK_PATH/examples~/regression-tests/procedure-client/module_bindings"/*/obj~ # Publish module for procedure tests -cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server local -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests +cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- publish -c -y --server "$SPACETIMEDB_SERVER_URL" -p "$STDB_PATH/modules/sdk-test-procedure" procedure-tests # Run client for btree test cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug From 2fdbeb4bcbde714db55b2b1593dae0fc55861afb Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 11:22:39 -0700 Subject: [PATCH 11/36] [bfops/cargo-test-all]: fixhelp --- tools/ci/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/ci/README.md b/tools/ci/README.md index 693631943c1..9d1928edf73 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -217,6 +217,17 @@ Usage: typescript-test - `--help`: Print help +### `csharp-tests` + +**Usage:** +```bash +Usage: csharp-tests +``` + +**Options:** + +- `--help`: Print help + ### `docs` **Usage:** From 1dac83166efd621fb62a6dbc1d66d77085903886 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 11:38:28 -0700 Subject: [PATCH 12/36] [bfops/cargo-test-all]: more --- crates/csharp-tests/tests/csharp.rs | 21 ++------------------- crates/typescript-tests/tests/typescript.rs | 16 ---------------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index b1d8a39be87..967e5e5247a 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -22,7 +22,6 @@ fn run() -> Result<()> { let workspace = spacetimedb_language_test_support::workspace_root(); let out_dir = artifact_dir("csharp")?; - run_bindings_tests(&workspace, &out_dir, &args)?; run_sdk_tests(&workspace, &out_dir, &args)?; if !args.list { run_regression_tests(&workspace)?; @@ -31,12 +30,6 @@ fn run() -> Result<()> { Ok(()) } -fn run_bindings_tests(workspace: &Path, out_dir: &Path, args: &HarnessArgs) -> Result<()> { - let cwd = workspace.join("crates/bindings-csharp"); - run_dotnet_test("csharp bindings", &cwd, out_dir, "bindings.trx", args, &[])?; - Ok(()) -} - fn run_sdk_tests(workspace: &Path, out_dir: &Path, args: &HarnessArgs) -> Result<()> { prepare_csharp_sdk_solution(workspace)?; let cwd = workspace.join("sdks/csharp"); @@ -112,22 +105,12 @@ fn find_trx(preferred: &Path, cwd: &Path) -> Result { fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { run_command( "dotnet", - &[ - "pack".to_string(), - "crates/bindings-csharp/BSATN.Runtime".to_string(), - "-c".to_string(), - "Release".to_string(), - ], + &["pack".to_string(), "crates/bindings-csharp/BSATN.Runtime".to_string()], workspace, )?; run_command( "dotnet", - &[ - "pack".to_string(), - "crates/bindings-csharp/Runtime".to_string(), - "-c".to_string(), - "Release".to_string(), - ], + &["pack".to_string(), "crates/bindings-csharp/Runtime".to_string()], workspace, )?; run_command( diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index a27667c34c3..e195864b24d 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -49,21 +49,5 @@ fn run() -> Result<()> { let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; - let typecheck_report = out_dir.join("vitest-typecheck.junit.xml"); - let typecheck_args = vec![ - "vitest".to_string(), - "typecheck".to_string(), - "--run".to_string(), - "--reporter=default".to_string(), - "--reporter=junit".to_string(), - format!("--outputFile={}", typecheck_report.display()), - ]; - run_command("pnpm", &typecheck_args, &cwd)?; - if typecheck_report.exists() { - let results = parse_junit(&typecheck_report) - .with_context(|| "failed to parse TypeScript Vitest typecheck JUnit report")?; - print_results("typescript typecheck", &typecheck_report, &results)?; - } - Ok(()) } From 3d089e1034813360af883cce2fd86b213dc92216 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 13:27:58 -0700 Subject: [PATCH 13/36] [bfops/cargo-test-all]: Address language test harness review --- Cargo.lock | 4 +- crates/csharp-tests/tests/csharp.rs | 7 +- crates/language-test-support/Cargo.toml | 4 +- crates/language-test-support/src/lib.rs | 171 ++++++++------------ crates/typescript-tests/tests/typescript.rs | 10 +- 5 files changed, 85 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53552244259..02c5b98cbd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8309,10 +8309,10 @@ name = "spacetimedb-language-test-support" version = "2.2.0" dependencies = [ "anyhow", + "clap 4.5.50", "quick-xml 0.31.0", - "serde_json", "spacetimedb-guard", - "tempfile", + "which 8.0.0", ] [[package]] diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 967e5e5247a..879c1ed90d1 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -2,8 +2,8 @@ use anyhow::{bail, Context, Result}; use spacetimedb_language_test_support::{ - artifact_dir, parse_trx, print_results, require_tool, run_command, run_command_env, run_command_forward, - HarnessArgs, SpacetimeDbGuard, + parse_trx, print_results, require_tool, run_command, run_command_env, run_command_forward, target_dir, HarnessArgs, + SpacetimeDbGuard, }; use std::fs; use std::path::{Path, PathBuf}; @@ -20,7 +20,8 @@ fn run() -> Result<()> { require_tool("dotnet")?; let workspace = spacetimedb_language_test_support::workspace_root(); - let out_dir = artifact_dir("csharp")?; + let out_dir = target_dir().join("language-tests").join("csharp"); + fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; run_sdk_tests(&workspace, &out_dir, &args)?; if !args.list { diff --git a/crates/language-test-support/Cargo.toml b/crates/language-test-support/Cargo.toml index b76c1c889ae..8a3465fef35 100644 --- a/crates/language-test-support/Cargo.toml +++ b/crates/language-test-support/Cargo.toml @@ -6,10 +6,10 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true +clap.workspace = true quick-xml.workspace = true -serde_json.workspace = true spacetimedb-guard.workspace = true -tempfile.workspace = true +which = "8.0.0" [lints] workspace = true diff --git a/crates/language-test-support/src/lib.rs b/crates/language-test-support/src/lib.rs index b5ba503ce08..a1dd826868b 100644 --- a/crates/language-test-support/src/lib.rs +++ b/crates/language-test-support/src/lib.rs @@ -1,13 +1,13 @@ #![allow(clippy::disallowed_macros)] use anyhow::{bail, Context, Result}; +use clap::Parser; use quick_xml::events::Event; use quick_xml::Reader; use std::env; use std::ffi::OsStr; -use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::{Command, Output, Stdio}; pub use spacetimedb_guard::SpacetimeDbGuard; @@ -25,45 +25,42 @@ pub struct TestCaseResult { pub message: Option, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Parser)] +#[command(disable_help_flag = true, about = "Runs a wrapped non-Rust language test suite")] pub struct HarnessArgs { + /// Filter native tests by name. + #[arg(long)] pub filter: Option, + + /// List native tests instead of running them. + #[arg(long, alias = "list-tests")] pub list: bool, + + #[arg(skip)] pub passthrough: Vec, + + #[arg()] + positional: Vec, } impl HarnessArgs { pub fn parse() -> Self { - let mut args = env::args().skip(1).peekable(); - let mut parsed = HarnessArgs::default(); + let mut args = env::args().collect::>(); + let passthrough = args + .iter() + .position(|arg| arg == "--") + .map(|index| args.split_off(index + 1)) + .unwrap_or_default(); + if args.last().is_some_and(|arg| arg == "--") { + args.pop(); + } - while let Some(arg) = args.next() { - match arg.as_str() { - "--filter" => { - parsed.filter = args.next(); - } - "--list" | "--list-tests" => { - parsed.list = true; - } - "--" => { - parsed.passthrough.extend(args); - break; - } - other if other.starts_with("--filter=") => { - parsed.filter = Some(other.trim_start_matches("--filter=").to_string()); - } - other if other.starts_with("--") => { - parsed.passthrough.push(other.to_string()); - } - other => { - // Match libtest's common shorthand: `cargo test foo`. - if parsed.filter.is_none() { - parsed.filter = Some(other.to_string()); - } else { - parsed.passthrough.push(other.to_string()); - } - } - } + let mut parsed = ::parse_from(args); + parsed.passthrough = passthrough; + if parsed.filter.is_none() + && let Some(filter) = parsed.positional.first() + { + parsed.filter = Some(filter.clone()); } parsed @@ -84,78 +81,45 @@ pub fn target_dir() -> PathBuf { .unwrap_or_else(|| workspace_root().join("target")) } -pub fn artifact_dir(suite: &str) -> Result { - let dir = target_dir().join("language-tests").join(suite); - fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?; - Ok(dir) -} - pub fn require_tool(tool: &str) -> Result<()> { - if find_on_path(tool).is_some() { + if which::which(tool).is_ok() { Ok(()) } else { bail!("required tool `{tool}` was not found on PATH") } } -fn find_on_path(tool: &str) -> Option { - let path = env::var_os("PATH")?; - let candidates = env::split_paths(&path).flat_map(|dir| executable_candidates(&dir, tool)); - candidates.into_iter().find(|candidate| candidate.is_file()) -} - -fn executable_candidates(dir: &Path, tool: &str) -> Vec { - #[cfg(windows)] - { - let mut candidates = vec![dir.join(tool)]; - if Path::new(tool).extension().is_none() { - let pathext = env::var_os("PATHEXT") - .map(|v| { - env::split_paths(&v) - .filter_map(|p| p.as_os_str().to_str().map(ToOwned::to_owned)) - .collect::>() - }) - .unwrap_or_else(|| vec![".exe".to_string(), ".bat".to_string(), ".cmd".to_string()]); - candidates.extend(pathext.into_iter().map(|ext| dir.join(format!("{tool}{ext}")))); - } - candidates - } - - #[cfg(not(windows))] - { - vec![dir.join(tool)] - } +pub fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { + run_command_inner(program, args, cwd, &[]) } -pub fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { - let output = Command::new(program) +pub fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { + let status = Command::new(program) .args(args) .current_dir(cwd) - .output() + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - - if !output.status.success() { - bail!( - "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - shell_line(program, args), - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - Ok(output) -} - -pub fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { - let output = run_command(program, args, cwd)?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success( + cwd, + program, + args, + &Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }, + )?; Ok(()) } pub fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { + run_command_inner(program, args, cwd, envs) +} + +fn run_command_inner(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { let output = Command::new(program) .args(args) .current_dir(cwd) @@ -163,20 +127,26 @@ pub fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str .output() .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - if !output.status.success() { - bail!( - "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - shell_line(program, args), - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } + ensure_success(cwd, program, args, &output)?; Ok(output) } +fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { + if output.status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + shell_line(program, args), + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + pub fn shell_line(program: &str, args: &[String]) -> String { std::iter::once(program.to_string()) .chain(args.iter().cloned()) @@ -192,12 +162,13 @@ pub fn parse_junit(path: &Path) -> Result> { parse_xml_results(path, XmlKind::Junit) } -enum XmlKind { +#[derive(Clone, Copy, Debug)] +pub enum XmlKind { Trx, Junit, } -fn parse_xml_results(path: &Path, kind: XmlKind) -> Result> { +pub fn parse_xml_results(path: &Path, kind: XmlKind) -> Result> { let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; reader.trim_text(true); diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index e195864b24d..f49df64aea2 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -2,8 +2,9 @@ use anyhow::{Context, Result}; use spacetimedb_language_test_support::{ - artifact_dir, parse_junit, print_results, require_tool, run_command, run_command_forward, HarnessArgs, + parse_junit, print_results, require_tool, run_command, run_command_forward, target_dir, HarnessArgs, }; +use std::fs; fn main() { if let Err(err) = run() { @@ -18,7 +19,8 @@ fn run() -> Result<()> { let workspace = spacetimedb_language_test_support::workspace_root(); let cwd = workspace.join("crates/bindings-typescript"); - let out_dir = artifact_dir("typescript")?; + let out_dir = target_dir().join("language-tests").join("typescript"); + fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; let report = out_dir.join("vitest.junit.xml"); if args.list { @@ -33,8 +35,8 @@ fn run() -> Result<()> { run_command("pnpm", &["build".to_string()], &cwd)?; let mut test_args = vec![ - "vitest".to_string(), - "run".to_string(), + "test".to_string(), + "--".to_string(), "--reporter=default".to_string(), "--reporter=junit".to_string(), format!("--outputFile={}", report.display()), From b183830f6540a44f166903e4be708d11fa809f79 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 16:40:32 -0700 Subject: [PATCH 14/36] [bfops/cargo-test-all]: Address follow-up harness review --- Cargo.lock | 1 - crates/csharp-tests/tests/csharp.rs | 6 ++---- crates/language-test-support/Cargo.toml | 1 - crates/language-test-support/src/lib.rs | 8 -------- crates/typescript-tests/tests/typescript.rs | 5 ++--- 5 files changed, 4 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02c5b98cbd1..1eb2d23f4f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8312,7 +8312,6 @@ dependencies = [ "clap 4.5.50", "quick-xml 0.31.0", "spacetimedb-guard", - "which 8.0.0", ] [[package]] diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 879c1ed90d1..1877f7236ad 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use spacetimedb_language_test_support::{ - parse_trx, print_results, require_tool, run_command, run_command_env, run_command_forward, target_dir, HarnessArgs, + parse_trx, print_results, run_command, run_command_env, run_command_forward, target_dir, HarnessArgs, SpacetimeDbGuard, }; use std::fs; @@ -17,10 +17,9 @@ fn main() { fn run() -> Result<()> { let args = HarnessArgs::parse(); - require_tool("dotnet")?; let workspace = spacetimedb_language_test_support::workspace_root(); - let out_dir = target_dir().join("language-tests").join("csharp"); + let out_dir = target_dir().join("csharp-tests"); fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; run_sdk_tests(&workspace, &out_dir, &args)?; @@ -133,7 +132,6 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { } fn run_regression_tests(workspace: &Path) -> Result<()> { - require_tool("bash")?; // The regression module itself still performs an HTTP egress call to // localhost:3000, so this specific suite needs the fixed listen address. let guard = SpacetimeDbGuard::spawn_in_temp_data_dir_with_listen_addr("127.0.0.1:3000"); diff --git a/crates/language-test-support/Cargo.toml b/crates/language-test-support/Cargo.toml index 8a3465fef35..31bc0ab6614 100644 --- a/crates/language-test-support/Cargo.toml +++ b/crates/language-test-support/Cargo.toml @@ -9,7 +9,6 @@ anyhow.workspace = true clap.workspace = true quick-xml.workspace = true spacetimedb-guard.workspace = true -which = "8.0.0" [lints] workspace = true diff --git a/crates/language-test-support/src/lib.rs b/crates/language-test-support/src/lib.rs index a1dd826868b..79293f19cbe 100644 --- a/crates/language-test-support/src/lib.rs +++ b/crates/language-test-support/src/lib.rs @@ -81,14 +81,6 @@ pub fn target_dir() -> PathBuf { .unwrap_or_else(|| workspace_root().join("target")) } -pub fn require_tool(tool: &str) -> Result<()> { - if which::which(tool).is_ok() { - Ok(()) - } else { - bail!("required tool `{tool}` was not found on PATH") - } -} - pub fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { run_command_inner(program, args, cwd, &[]) } diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index f49df64aea2..d62e567c017 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use spacetimedb_language_test_support::{ - parse_junit, print_results, require_tool, run_command, run_command_forward, target_dir, HarnessArgs, + parse_junit, print_results, run_command, run_command_forward, target_dir, HarnessArgs, }; use std::fs; @@ -15,11 +15,10 @@ fn main() { fn run() -> Result<()> { let args = HarnessArgs::parse(); - require_tool("pnpm")?; let workspace = spacetimedb_language_test_support::workspace_root(); let cwd = workspace.join("crates/bindings-typescript"); - let out_dir = target_dir().join("language-tests").join("typescript"); + let out_dir = target_dir().join("typescript-tests"); fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; let report = out_dir.join("vitest.junit.xml"); From 107ebc62afb4f9e6b3f8d9679161b3acc2ab9a94 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 16:53:21 -0700 Subject: [PATCH 15/36] [bfops/cargo-test-all]: Address unresolved harness review --- Cargo.lock | 6 +- crates/csharp-tests/Cargo.toml | 2 + crates/csharp-tests/tests/csharp.rs | 165 +++++++++++++- crates/guard/src/lib.rs | 30 +-- crates/language-test-support/Cargo.toml | 2 - crates/language-test-support/src/lib.rs | 207 +----------------- crates/typescript-tests/Cargo.toml | 2 + crates/typescript-tests/tests/typescript.rs | 171 ++++++++++++++- modules/sdk-test-procedure/src/lib.rs | 9 +- .../regression-tests/client/Program.cs | 1 + .../Procedures/ReadMySchemaViaHttp.g.cs | 21 +- .../Procedures/ReadMySchema.g.cs | 21 +- .../examples~/regression-tests/server/Lib.cs | 4 +- 13 files changed, 380 insertions(+), 261 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1eb2d23f4f0..a51c74638ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8141,6 +8141,8 @@ name = "spacetimedb-csharp-tests" version = "2.2.0" dependencies = [ "anyhow", + "clap 4.5.50", + "quick-xml 0.31.0", "spacetimedb-language-test-support", ] @@ -8309,8 +8311,6 @@ name = "spacetimedb-language-test-support" version = "2.2.0" dependencies = [ "anyhow", - "clap 4.5.50", - "quick-xml 0.31.0", "spacetimedb-guard", ] @@ -8807,6 +8807,8 @@ name = "spacetimedb-typescript-tests" version = "2.2.0" dependencies = [ "anyhow", + "clap 4.5.50", + "quick-xml 0.31.0", "spacetimedb-language-test-support", ] diff --git a/crates/csharp-tests/Cargo.toml b/crates/csharp-tests/Cargo.toml index b0d97feacc6..706271f5d29 100644 --- a/crates/csharp-tests/Cargo.toml +++ b/crates/csharp-tests/Cargo.toml @@ -12,6 +12,8 @@ harness = false [dev-dependencies] anyhow.workspace = true +clap.workspace = true +quick-xml.workspace = true spacetimedb-language-test-support.workspace = true [lints] diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 1877f7236ad..56ea05fea4d 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -1,12 +1,54 @@ #![allow(clippy::disallowed_macros)] use anyhow::{bail, Context, Result}; -use spacetimedb_language_test_support::{ - parse_trx, print_results, run_command, run_command_env, run_command_forward, target_dir, HarnessArgs, - SpacetimeDbGuard, -}; +use clap::Parser; +use quick_xml::events::Event; +use quick_xml::Reader; +use spacetimedb_language_test_support::{print_results, target_dir, Outcome, SpacetimeDbGuard, TestCaseResult}; +use std::env; +use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; + +#[derive(Clone, Debug, Default, Parser)] +#[command(disable_help_flag = true)] +struct Args { + #[arg(long)] + filter: Option, + + #[arg(long, alias = "list-tests")] + list: bool, + + #[arg(skip)] + passthrough: Vec, + + #[arg()] + positional: Vec, +} + +impl Args { + fn parse() -> Self { + let mut args = env::args().collect::>(); + let passthrough = args + .iter() + .position(|arg| arg == "--") + .map(|index| args.split_off(index + 1)) + .unwrap_or_default(); + if args.last().is_some_and(|arg| arg == "--") { + args.pop(); + } + + let mut parsed = ::parse_from(args); + parsed.passthrough = passthrough; + if parsed.filter.is_none() + && let Some(filter) = parsed.positional.first() + { + parsed.filter = Some(filter.clone()); + } + parsed + } +} fn main() { if let Err(err) = run() { @@ -16,7 +58,7 @@ fn main() { } fn run() -> Result<()> { - let args = HarnessArgs::parse(); + let args = Args::parse(); let workspace = spacetimedb_language_test_support::workspace_root(); let out_dir = target_dir().join("csharp-tests"); @@ -30,7 +72,7 @@ fn run() -> Result<()> { Ok(()) } -fn run_sdk_tests(workspace: &Path, out_dir: &Path, args: &HarnessArgs) -> Result<()> { +fn run_sdk_tests(workspace: &Path, out_dir: &Path, args: &Args) -> Result<()> { prepare_csharp_sdk_solution(workspace)?; let cwd = workspace.join("sdks/csharp"); run_dotnet_test( @@ -49,7 +91,7 @@ fn run_dotnet_test( cwd: &Path, out_dir: &Path, report_name: &str, - args: &HarnessArgs, + args: &Args, extra_args: &[String], ) -> Result<()> { let report = out_dir.join(report_name); @@ -132,9 +174,7 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { } fn run_regression_tests(workspace: &Path) -> Result<()> { - // The regression module itself still performs an HTTP egress call to - // localhost:3000, so this specific suite needs the fixed listen address. - let guard = SpacetimeDbGuard::spawn_in_temp_data_dir_with_listen_addr("127.0.0.1:3000"); + let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); run_command_env( "bash", &["tools~/run-regression-tests.sh".to_string()], @@ -143,3 +183,108 @@ fn run_regression_tests(workspace: &Path) -> Result<()> { )?; Ok(()) } + +fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { + run_command_inner(program, args, cwd, &[]) +} + +fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { + run_command_inner(program, args, cwd, envs) +} + +fn run_command_inner(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { + let output = Command::new(program) + .args(args) + .current_dir(cwd) + .envs(envs.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)))) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + ensure_success(cwd, program, args, &output)?; + Ok(output) +} + +fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { + let status = Command::new(program) + .args(args) + .current_dir(cwd) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + ensure_success( + cwd, + program, + args, + &Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }, + ) +} + +fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { + if output.status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + shell_line(program, args), + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn shell_line(program: &str, args: &[String]) -> String { + std::iter::once(program.to_string()) + .chain(args.iter().cloned()) + .collect::>() + .join(" ") +} + +fn parse_trx(path: &Path) -> Result> { + let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; + reader.trim_text(true); + + let mut buf = Vec::new(); + let mut results = Vec::new(); + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Empty(e)) | Ok(Event::Start(e)) if e.name().as_ref() == b"UnitTestResult" => { + let name = attr(&e, b"testName")?.unwrap_or_else(|| "".to_string()); + let outcome = match attr(&e, b"outcome")?.as_deref() { + Some("Passed") => Outcome::Passed, + Some("NotExecuted") => Outcome::Skipped, + Some("Failed") => Outcome::Failed, + _ => Outcome::Failed, + }; + results.push(TestCaseResult { + name, + outcome, + message: None, + }); + } + Ok(Event::Eof) => break, + Err(err) => bail!("failed to parse {}: {err}", path.display()), + _ => {} + } + buf.clear(); + } + + Ok(results) +} + +fn attr(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Result> { + for attr in e.attributes() { + let attr = attr?; + if attr.key.as_ref() == key { + return Ok(Some(String::from_utf8_lossy(attr.value.as_ref()).to_string())); + } + } + Ok(None) +} diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index c296212bb8b..f10e1e6fbce 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -123,15 +123,7 @@ impl SpacetimeDbGuard { let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); let data_dir_path = temp_dir.path().to_path_buf(); - Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir_path, Some(temp_dir), "127.0.0.1:0") - } - - /// Start `spacetimedb` in a temporary data directory on a specific listen address. - pub fn spawn_in_temp_data_dir_with_listen_addr(listen_addr: &str) -> Self { - let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); - let data_dir_path = temp_dir.path().to_path_buf(); - - Self::spawn_spacetime_start_with_data_dir(false, None, data_dir_path, Some(temp_dir), listen_addr) + Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir_path, Some(temp_dir)) } /// Start `spacetimedb` in a temporary data directory via: @@ -140,7 +132,7 @@ impl SpacetimeDbGuard { let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); let data_dir_path = temp_dir.path().to_path_buf(); - Self::spawn_spacetime_start_with_data_dir(true, None, data_dir_path, Some(temp_dir), "127.0.0.1:0") + Self::spawn_spacetime_start_with_data_dir(true, None, data_dir_path, Some(temp_dir)) } /// Start `spacetimedb` with an explicit data directory (for restart scenarios). @@ -148,7 +140,7 @@ impl SpacetimeDbGuard { /// Unlike `spawn_in_temp_data_dir`, this method does not create a temporary directory. /// The caller is responsible for managing the data directory lifetime. pub fn spawn_with_data_dir(data_dir: PathBuf, pg_port: Option) -> Self { - Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir, None, "127.0.0.1:0") + Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir, None) } fn spawn_spacetime_start_with_data_dir( @@ -156,11 +148,10 @@ impl SpacetimeDbGuard { pg_port: Option, data_dir: PathBuf, _data_dir_handle: Option, - listen_addr: &str, ) -> Self { let spawn_id = next_spawn_id(); let (child, logs, host_url, reader_threads) = - Self::spawn_server(&data_dir, pg_port, spawn_id, use_installed_cli, listen_addr); + Self::spawn_server(&data_dir, pg_port, spawn_id, use_installed_cli); SpacetimeDbGuard { child, host_url, @@ -197,13 +188,8 @@ impl SpacetimeDbGuard { sleep(Duration::from_millis(100)); eprintln!("[RESTART-{:03}] Spawning new server", spawn_id); - let (child, logs, host_url, reader_threads) = Self::spawn_server( - &self.data_dir, - self.pg_port, - spawn_id, - self.use_installed_cli, - "127.0.0.1:0", - ); + let (child, logs, host_url, reader_threads) = + Self::spawn_server(&self.data_dir, self.pg_port, spawn_id, self.use_installed_cli); eprintln!( "[RESTART-{:03}] New server ready, pid={}, url={}", spawn_id, @@ -266,7 +252,6 @@ impl SpacetimeDbGuard { pg_port: Option, spawn_id: u64, use_installed_cli: bool, - listen_addr: &str, ) -> (Child, Arc>, String, Vec>) { let cmd = if use_installed_cli { eprintln!("[SPAWN-{:03}] START Using installed CLI", spawn_id); @@ -283,6 +268,7 @@ impl SpacetimeDbGuard { let data_dir_str = data_dir.display().to_string(); let pg_port_str = pg_port.map(|p| p.to_string()); + let address = "127.0.0.1:0".to_string(); let mut args = vec![ "start", @@ -291,7 +277,7 @@ impl SpacetimeDbGuard { "--data-dir", &data_dir_str, "--listen-addr", - listen_addr, + &address, ]; if let Some(ref port) = pg_port_str { args.extend(["--pg-port", port]); diff --git a/crates/language-test-support/Cargo.toml b/crates/language-test-support/Cargo.toml index 31bc0ab6614..eefe24857ac 100644 --- a/crates/language-test-support/Cargo.toml +++ b/crates/language-test-support/Cargo.toml @@ -6,8 +6,6 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true -clap.workspace = true -quick-xml.workspace = true spacetimedb-guard.workspace = true [lints] diff --git a/crates/language-test-support/src/lib.rs b/crates/language-test-support/src/lib.rs index 79293f19cbe..7f2d770fa46 100644 --- a/crates/language-test-support/src/lib.rs +++ b/crates/language-test-support/src/lib.rs @@ -1,13 +1,8 @@ #![allow(clippy::disallowed_macros)] -use anyhow::{bail, Context, Result}; -use clap::Parser; -use quick_xml::events::Event; -use quick_xml::Reader; +use anyhow::{bail, Result}; use std::env; -use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::process::{Command, Output, Stdio}; pub use spacetimedb_guard::SpacetimeDbGuard; @@ -25,48 +20,6 @@ pub struct TestCaseResult { pub message: Option, } -#[derive(Clone, Debug, Default, Parser)] -#[command(disable_help_flag = true, about = "Runs a wrapped non-Rust language test suite")] -pub struct HarnessArgs { - /// Filter native tests by name. - #[arg(long)] - pub filter: Option, - - /// List native tests instead of running them. - #[arg(long, alias = "list-tests")] - pub list: bool, - - #[arg(skip)] - pub passthrough: Vec, - - #[arg()] - positional: Vec, -} - -impl HarnessArgs { - pub fn parse() -> Self { - let mut args = env::args().collect::>(); - let passthrough = args - .iter() - .position(|arg| arg == "--") - .map(|index| args.split_off(index + 1)) - .unwrap_or_default(); - if args.last().is_some_and(|arg| arg == "--") { - args.pop(); - } - - let mut parsed = ::parse_from(args); - parsed.passthrough = passthrough; - if parsed.filter.is_none() - && let Some(filter) = parsed.positional.first() - { - parsed.filter = Some(filter.clone()); - } - - parsed - } -} - pub fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -81,164 +34,6 @@ pub fn target_dir() -> PathBuf { .unwrap_or_else(|| workspace_root().join("target")) } -pub fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { - run_command_inner(program, args, cwd, &[]) -} - -pub fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { - let status = Command::new(program) - .args(args) - .current_dir(cwd) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - ensure_success( - cwd, - program, - args, - &Output { - status, - stdout: Vec::new(), - stderr: Vec::new(), - }, - )?; - Ok(()) -} - -pub fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { - run_command_inner(program, args, cwd, envs) -} - -fn run_command_inner(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { - let output = Command::new(program) - .args(args) - .current_dir(cwd) - .envs(envs.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)))) - .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - - ensure_success(cwd, program, args, &output)?; - - Ok(output) -} - -fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { - if output.status.success() { - return Ok(()); - } - - bail!( - "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - cwd.display(), - shell_line(program, args), - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -pub fn shell_line(program: &str, args: &[String]) -> String { - std::iter::once(program.to_string()) - .chain(args.iter().cloned()) - .collect::>() - .join(" ") -} - -pub fn parse_trx(path: &Path) -> Result> { - parse_xml_results(path, XmlKind::Trx) -} - -pub fn parse_junit(path: &Path) -> Result> { - parse_xml_results(path, XmlKind::Junit) -} - -#[derive(Clone, Copy, Debug)] -pub enum XmlKind { - Trx, - Junit, -} - -pub fn parse_xml_results(path: &Path, kind: XmlKind) -> Result> { - let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; - reader.trim_text(true); - - let mut buf = Vec::new(); - let mut results = Vec::new(); - let mut current_junit: Option = None; - - loop { - match reader.read_event_into(&mut buf) { - Ok(Event::Empty(e)) | Ok(Event::Start(e)) => match (e.name().as_ref(), &kind) { - (b"UnitTestResult", XmlKind::Trx) => { - let name = attr(&e, b"testName")?.unwrap_or_else(|| "".to_string()); - let outcome = match attr(&e, b"outcome")?.as_deref() { - Some("Passed") => Outcome::Passed, - Some("NotExecuted") => Outcome::Skipped, - Some("Failed") => Outcome::Failed, - _ => Outcome::Failed, - }; - results.push(TestCaseResult { - name, - outcome, - message: None, - }); - } - (b"testcase", XmlKind::Junit) => { - let name = attr(&e, b"name")?.unwrap_or_else(|| "".to_string()); - let class = attr(&e, b"classname")?; - let name = class.map(|class| format!("{class}::{name}")).unwrap_or(name); - current_junit = Some(TestCaseResult { - name, - outcome: Outcome::Passed, - message: None, - }); - if e.is_empty() - && let Some(case) = current_junit.take() - { - results.push(case); - } - } - (b"failure" | b"error", XmlKind::Junit) => { - if let Some(case) = current_junit.as_mut() { - case.outcome = Outcome::Failed; - case.message = attr(&e, b"message")?; - } - } - (b"skipped", XmlKind::Junit) => { - if let Some(case) = current_junit.as_mut() { - case.outcome = Outcome::Skipped; - case.message = attr(&e, b"message")?; - } - } - _ => {} - }, - Ok(Event::End(e)) if matches!((&kind, e.name().as_ref()), (XmlKind::Junit, b"testcase")) => { - if let Some(case) = current_junit.take() { - results.push(case); - } - } - Ok(Event::Eof) => break, - Err(err) => bail!("failed to parse {}: {err}", path.display()), - _ => {} - } - buf.clear(); - } - - Ok(results) -} - -fn attr(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Result> { - for attr in e.attributes() { - let attr = attr?; - if attr.key.as_ref() == key { - return Ok(Some(String::from_utf8_lossy(attr.value.as_ref()).to_string())); - } - } - Ok(None) -} - pub fn print_results(suite: &str, report_path: &Path, results: &[TestCaseResult]) -> Result<()> { let passed = results.iter().filter(|r| r.outcome == Outcome::Passed).count(); let failed = results.iter().filter(|r| r.outcome == Outcome::Failed).count(); diff --git a/crates/typescript-tests/Cargo.toml b/crates/typescript-tests/Cargo.toml index 2994449d99c..3e97833dc7b 100644 --- a/crates/typescript-tests/Cargo.toml +++ b/crates/typescript-tests/Cargo.toml @@ -12,6 +12,8 @@ harness = false [dev-dependencies] anyhow.workspace = true +clap.workspace = true +quick-xml.workspace = true spacetimedb-language-test-support.workspace = true [lints] diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index d62e567c017..975479b5dae 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -1,10 +1,53 @@ #![allow(clippy::disallowed_macros)] -use anyhow::{Context, Result}; -use spacetimedb_language_test_support::{ - parse_junit, print_results, run_command, run_command_forward, target_dir, HarnessArgs, -}; +use anyhow::{bail, Context, Result}; +use clap::Parser; +use quick_xml::events::Event; +use quick_xml::Reader; +use spacetimedb_language_test_support::{print_results, target_dir, Outcome, TestCaseResult}; +use std::env; use std::fs; +use std::path::Path; +use std::process::{Command, Output, Stdio}; + +#[derive(Clone, Debug, Default, Parser)] +#[command(disable_help_flag = true)] +struct Args { + #[arg(long)] + filter: Option, + + #[arg(long, alias = "list-tests")] + list: bool, + + #[arg(skip)] + passthrough: Vec, + + #[arg()] + positional: Vec, +} + +impl Args { + fn parse() -> Self { + let mut args = env::args().collect::>(); + let passthrough = args + .iter() + .position(|arg| arg == "--") + .map(|index| args.split_off(index + 1)) + .unwrap_or_default(); + if args.last().is_some_and(|arg| arg == "--") { + args.pop(); + } + + let mut parsed = ::parse_from(args); + parsed.passthrough = passthrough; + if parsed.filter.is_none() + && let Some(filter) = parsed.positional.first() + { + parsed.filter = Some(filter.clone()); + } + parsed + } +} fn main() { if let Err(err) = run() { @@ -14,7 +57,7 @@ fn main() { } fn run() -> Result<()> { - let args = HarnessArgs::parse(); + let args = Args::parse(); let workspace = spacetimedb_language_test_support::workspace_root(); let cwd = workspace.join("crates/bindings-typescript"); @@ -52,3 +95,121 @@ fn run() -> Result<()> { Ok(()) } + +fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { + let output = Command::new(program) + .args(args) + .current_dir(cwd) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + ensure_success(cwd, program, args, &output)?; + Ok(output) +} + +fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { + let status = Command::new(program) + .args(args) + .current_dir(cwd) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; + ensure_success( + cwd, + program, + args, + &Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }, + ) +} + +fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { + if output.status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + cwd.display(), + shell_line(program, args), + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn shell_line(program: &str, args: &[String]) -> String { + std::iter::once(program.to_string()) + .chain(args.iter().cloned()) + .collect::>() + .join(" ") +} + +fn parse_junit(path: &Path) -> Result> { + let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; + reader.trim_text(true); + + let mut buf = Vec::new(); + let mut results = Vec::new(); + let mut current: Option = None; + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Empty(e)) | Ok(Event::Start(e)) => match e.name().as_ref() { + b"testcase" => { + let name = attr(&e, b"name")?.unwrap_or_else(|| "".to_string()); + let class = attr(&e, b"classname")?; + let name = class.map(|class| format!("{class}::{name}")).unwrap_or(name); + current = Some(TestCaseResult { + name, + outcome: Outcome::Passed, + message: None, + }); + if e.is_empty() + && let Some(case) = current.take() + { + results.push(case); + } + } + b"failure" | b"error" => { + if let Some(case) = current.as_mut() { + case.outcome = Outcome::Failed; + case.message = attr(&e, b"message")?; + } + } + b"skipped" => { + if let Some(case) = current.as_mut() { + case.outcome = Outcome::Skipped; + case.message = attr(&e, b"message")?; + } + } + _ => {} + }, + Ok(Event::End(e)) if e.name().as_ref() == b"testcase" => { + if let Some(case) = current.take() { + results.push(case); + } + } + Ok(Event::Eof) => break, + Err(err) => bail!("failed to parse {}: {err}", path.display()), + _ => {} + } + buf.clear(); + } + + Ok(results) +} + +fn attr(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Result> { + for attr in e.attributes() { + let attr = attr?; + if attr.key.as_ref() == key { + return Ok(Some(String::from_utf8_lossy(attr.value.as_ref()).to_string())); + } + } + Ok(None) +} diff --git a/modules/sdk-test-procedure/src/lib.rs b/modules/sdk-test-procedure/src/lib.rs index 95ae9b523b1..22468edda3f 100644 --- a/modules/sdk-test-procedure/src/lib.rs +++ b/modules/sdk-test-procedure/src/lib.rs @@ -41,11 +41,12 @@ fn will_panic(_ctx: &mut ProcedureContext) { } #[procedure] -fn read_my_schema(ctx: &mut ProcedureContext) -> String { +fn read_my_schema(ctx: &mut ProcedureContext, server_url: String) -> String { let module_identity = ctx.identity(); - match ctx.http.get(format!( - "http://localhost:3000/v1/database/{module_identity}/schema?version=9" - )) { + match ctx + .http + .get(format!("{server_url}/v1/database/{module_identity}/schema?version=9")) + { Ok(result) => result.into_body().into_string_lossy(), Err(e) => panic!("{e}"), } diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs index c3b88d1b05a..bdba032c259 100644 --- a/sdks/csharp/examples~/regression-tests/client/Program.cs +++ b/sdks/csharp/examples~/regression-tests/client/Program.cs @@ -1244,6 +1244,7 @@ void OnSubscriptionApplied(SubscriptionEventContext context) Log.Debug("Calling ReadMySchemaViaHttp"); waiting++; context.Procedures.ReadMySchemaViaHttp( + HOST, (IProcedureEventContext ctx, ProcedureCallbackResult result) => { try diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs index 7d4c11042c6..ae52ad10eb4 100644 --- a/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs +++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs @@ -12,10 +12,10 @@ namespace SpacetimeDB.Types { public sealed partial class RemoteProcedures : RemoteBase { - public void ReadMySchemaViaHttp(ProcedureCallback callback) + public void ReadMySchemaViaHttp(string serverUrl, ProcedureCallback callback) { // Convert the clean callback to the wrapper callback - InternalReadMySchemaViaHttp((ctx, result) => + InternalReadMySchemaViaHttp(serverUrl, (ctx, result) => { if (result.IsSuccess && result.Value != null) { @@ -28,9 +28,9 @@ public void ReadMySchemaViaHttp(ProcedureCallback callback) }); } - private void InternalReadMySchemaViaHttp(ProcedureCallback callback) + private void InternalReadMySchemaViaHttp(string serverUrl, ProcedureCallback callback) { - conn.InternalCallProcedure(new Procedure.ReadMySchemaViaHttpArgs(), callback); + conn.InternalCallProcedure(new Procedure.ReadMySchemaViaHttpArgs(serverUrl), callback); } } @@ -58,6 +58,19 @@ public ReadMySchemaViaHttp() [DataContract] public sealed partial class ReadMySchemaViaHttpArgs : Procedure, IProcedureArgs { + [DataMember(Name = "server_url")] + public string ServerUrl; + + public ReadMySchemaViaHttpArgs(string ServerUrl) + { + this.ServerUrl = ServerUrl; + } + + public ReadMySchemaViaHttpArgs() + { + this.ServerUrl = ""; + } + string IProcedureArgs.ProcedureName => "read_my_schema_via_http"; } diff --git a/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/Procedures/ReadMySchema.g.cs b/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/Procedures/ReadMySchema.g.cs index 0ec376947f5..76ab7984fe9 100644 --- a/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/Procedures/ReadMySchema.g.cs +++ b/sdks/csharp/examples~/regression-tests/procedure-client/module_bindings/Procedures/ReadMySchema.g.cs @@ -12,10 +12,10 @@ namespace SpacetimeDB.Types { public sealed partial class RemoteProcedures : RemoteBase { - public void ReadMySchema(ProcedureCallback callback) + public void ReadMySchema(string serverUrl, ProcedureCallback callback) { // Convert the clean callback to the wrapper callback - InternalReadMySchema((ctx, result) => + InternalReadMySchema(serverUrl, (ctx, result) => { if (result.IsSuccess && result.Value != null) { @@ -28,9 +28,9 @@ public void ReadMySchema(ProcedureCallback callback) }); } - private void InternalReadMySchema(ProcedureCallback callback) + private void InternalReadMySchema(string serverUrl, ProcedureCallback callback) { - conn.InternalCallProcedure(new Procedure.ReadMySchemaArgs(), callback); + conn.InternalCallProcedure(new Procedure.ReadMySchemaArgs(serverUrl), callback); } } @@ -58,6 +58,19 @@ public ReadMySchema() [DataContract] public sealed partial class ReadMySchemaArgs : Procedure, IProcedureArgs { + [DataMember(Name = "server_url")] + public string ServerUrl; + + public ReadMySchemaArgs(string ServerUrl) + { + this.ServerUrl = ServerUrl; + } + + public ReadMySchemaArgs() + { + this.ServerUrl = ""; + } + string IProcedureArgs.ProcedureName => "read_my_schema"; } diff --git a/sdks/csharp/examples~/regression-tests/server/Lib.cs b/sdks/csharp/examples~/regression-tests/server/Lib.cs index 0644115876c..cf0fe792540 100644 --- a/sdks/csharp/examples~/regression-tests/server/Lib.cs +++ b/sdks/csharp/examples~/regression-tests/server/Lib.cs @@ -799,12 +799,12 @@ public static SpacetimeDB.Unit WillPanic(ProcedureContext ctx) [SpacetimeDB.Procedure] [Experimental("STDB_UNSTABLE")] - public static string ReadMySchemaViaHttp(ProcedureContext ctx) + public static string ReadMySchemaViaHttp(ProcedureContext ctx, string serverUrl) { try { var moduleIdentity = ProcedureContext.Identity; - var uri = $"http://localhost:3000/v1/database/{moduleIdentity}/schema?version=9"; + var uri = $"{serverUrl}/v1/database/{moduleIdentity}/schema?version=9"; var res = ctx.Http.Get(uri, System.TimeSpan.FromSeconds(2)); return res switch { From 6fdf577fdfae3a534d0f9c26fe4f0f631eca5ce8 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 16:59:05 -0700 Subject: [PATCH 16/36] [bfops/cargo-test-all]: revert --- crates/guard/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index f10e1e6fbce..af937910a54 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -268,6 +268,7 @@ impl SpacetimeDbGuard { let data_dir_str = data_dir.display().to_string(); let pg_port_str = pg_port.map(|p| p.to_string()); + let address = "127.0.0.1:0".to_string(); let mut args = vec![ From 746a3ab3d4d774bb23e794aa95e45d292912601d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 17:06:52 -0700 Subject: [PATCH 17/36] [bfops/cargo-test-all]: Address follow-up harness comments --- crates/csharp-tests/tests/csharp.rs | 179 +++++++++++--------- crates/typescript-tests/tests/typescript.rs | 11 +- 2 files changed, 96 insertions(+), 94 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 56ea05fea4d..3d3c0ce6b70 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -5,8 +5,6 @@ use clap::Parser; use quick_xml::events::Event; use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, SpacetimeDbGuard, TestCaseResult}; -use std::env; -use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; @@ -22,14 +20,11 @@ struct Args { #[arg(skip)] passthrough: Vec, - - #[arg()] - positional: Vec, } impl Args { fn parse() -> Self { - let mut args = env::args().collect::>(); + let mut args = std::env::args().collect::>(); let passthrough = args .iter() .position(|arg| arg == "--") @@ -41,11 +36,6 @@ impl Args { let mut parsed = ::parse_from(args); parsed.passthrough = passthrough; - if parsed.filter.is_none() - && let Some(filter) = parsed.positional.first() - { - parsed.filter = Some(filter.clone()); - } parsed } } @@ -99,7 +89,30 @@ fn run_dotnet_test( if args.list { let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; list_args.extend(extra_args.iter().cloned()); - run_command_forward("dotnet", &list_args, cwd)?; + let status = Command::new("dotnet") + .args(&list_args) + .current_dir(cwd) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("dotnet", &list_args), + cwd.display() + ) + })?; + ensure_success( + cwd, + "dotnet", + &list_args, + &Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }, + )?; return Ok(()); } @@ -118,7 +131,18 @@ fn run_dotnet_test( } test_args.extend(args.passthrough.iter().cloned()); - run_command("dotnet", &test_args, cwd)?; + let output = Command::new("dotnet") + .args(&test_args) + .current_dir(cwd) + .output() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("dotnet", &test_args), + cwd.display() + ) + })?; + ensure_success(cwd, "dotnet", &test_args, &output)?; let actual_report = find_trx(&report, cwd).with_context(|| format!("failed to locate TRX report for {suite}"))?; let results = parse_trx(&actual_report).with_context(|| format!("failed to parse {suite} TRX report"))?; print_results(suite, &actual_report, &results)?; @@ -145,83 +169,70 @@ fn find_trx(preferred: &Path, cwd: &Path) -> Result { } fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { - run_command( - "dotnet", - &["pack".to_string(), "crates/bindings-csharp/BSATN.Runtime".to_string()], - workspace, - )?; - run_command( - "dotnet", - &["pack".to_string(), "crates/bindings-csharp/Runtime".to_string()], - workspace, - )?; - run_command( - "bash", - &["./tools~/write-nuget-config.sh".to_string(), "../..".to_string()], - &workspace.join("sdks/csharp"), - )?; - run_command( - "dotnet", - &[ - "restore".to_string(), - "--configfile".to_string(), - "NuGet.Config".to_string(), - "SpacetimeDB.ClientSDK.sln".to_string(), - ], - &workspace.join("sdks/csharp"), - )?; + let args = ["pack".to_string(), "crates/bindings-csharp/BSATN.Runtime".to_string()]; + let output = Command::new("dotnet") + .args(&args) + .current_dir(workspace) + .output() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("dotnet", &args), + workspace.display() + ) + })?; + ensure_success(workspace, "dotnet", &args, &output)?; + + let args = ["pack".to_string(), "crates/bindings-csharp/Runtime".to_string()]; + let output = Command::new("dotnet") + .args(&args) + .current_dir(workspace) + .output() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("dotnet", &args), + workspace.display() + ) + })?; + ensure_success(workspace, "dotnet", &args, &output)?; + + let cwd = workspace.join("sdks/csharp"); + let args = ["./tools~/write-nuget-config.sh".to_string(), "../..".to_string()]; + let output = Command::new("bash") + .args(&args) + .current_dir(&cwd) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line("bash", &args), cwd.display()))?; + ensure_success(&cwd, "bash", &args, &output)?; + + let args = [ + "restore".to_string(), + "--configfile".to_string(), + "NuGet.Config".to_string(), + "SpacetimeDB.ClientSDK.sln".to_string(), + ]; + let output = Command::new("dotnet") + .args(&args) + .current_dir(&cwd) + .output() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line("dotnet", &args), cwd.display()))?; + ensure_success(&cwd, "dotnet", &args, &output)?; Ok(()) } fn run_regression_tests(workspace: &Path) -> Result<()> { let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); - run_command_env( - "bash", - &["tools~/run-regression-tests.sh".to_string()], - &workspace.join("sdks/csharp"), - &[("SPACETIMEDB_SERVER_URL", guard.host_url.clone())], - )?; - Ok(()) -} - -fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { - run_command_inner(program, args, cwd, &[]) -} - -fn run_command_env(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { - run_command_inner(program, args, cwd, envs) -} - -fn run_command_inner(program: &str, args: &[String], cwd: &Path, envs: &[(&str, String)]) -> Result { - let output = Command::new(program) - .args(args) - .current_dir(cwd) - .envs(envs.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)))) + let cwd = workspace.join("sdks/csharp"); + let args = ["tools~/run-regression-tests.sh".to_string()]; + let output = Command::new("bash") + .args(&args) + .current_dir(&cwd) + .env("SPACETIMEDB_SERVER_URL", &guard.host_url) .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - ensure_success(cwd, program, args, &output)?; - Ok(output) -} - -fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { - let status = Command::new(program) - .args(args) - .current_dir(cwd) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - ensure_success( - cwd, - program, - args, - &Output { - status, - stdout: Vec::new(), - stderr: Vec::new(), - }, - ) + .with_context(|| format!("failed to spawn `{}` in {}", shell_line("bash", &args), cwd.display()))?; + ensure_success(&cwd, "bash", &args, &output)?; + Ok(()) } fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 975479b5dae..3d36deadb9d 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -5,7 +5,6 @@ use clap::Parser; use quick_xml::events::Event; use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, TestCaseResult}; -use std::env; use std::fs; use std::path::Path; use std::process::{Command, Output, Stdio}; @@ -21,14 +20,11 @@ struct Args { #[arg(skip)] passthrough: Vec, - - #[arg()] - positional: Vec, } impl Args { fn parse() -> Self { - let mut args = env::args().collect::>(); + let mut args = std::env::args().collect::>(); let passthrough = args .iter() .position(|arg| arg == "--") @@ -40,11 +36,6 @@ impl Args { let mut parsed = ::parse_from(args); parsed.passthrough = passthrough; - if parsed.filter.is_none() - && let Some(filter) = parsed.positional.first() - { - parsed.filter = Some(filter.clone()); - } parsed } } From cc6d732f5d049fc0433d20aab1f7c9708b8aadad Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 17:10:38 -0700 Subject: [PATCH 18/36] [bfops/cargo-test-all]: Inline TypeScript harness commands --- crates/typescript-tests/tests/typescript.rs | 77 ++++++++++++--------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 3d36deadb9d..016903f630f 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -61,11 +61,40 @@ fn run() -> Result<()> { if let Some(filter) = args.filter { cmd.push(filter); } - run_command_forward("pnpm", &cmd, &cwd)?; + let status = Command::new("pnpm") + .args(&cmd) + .current_dir(&cwd) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line("pnpm", &cmd), cwd.display()))?; + ensure_success( + &cwd, + "pnpm", + &cmd, + &Output { + status, + stdout: Vec::new(), + stderr: Vec::new(), + }, + )?; return Ok(()); } - run_command("pnpm", &["build".to_string()], &cwd)?; + let build_args = ["build".to_string()]; + let output = Command::new("pnpm") + .args(&build_args) + .current_dir(&cwd) + .output() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("pnpm", &build_args), + cwd.display() + ) + })?; + ensure_success(&cwd, "pnpm", &build_args, &output)?; let mut test_args = vec![ "test".to_string(), @@ -79,7 +108,18 @@ fn run() -> Result<()> { test_args.push(filter); } test_args.extend(args.passthrough); - run_command("pnpm", &test_args, &cwd)?; + let output = Command::new("pnpm") + .args(&test_args) + .current_dir(&cwd) + .output() + .with_context(|| { + format!( + "failed to spawn `{}` in {}", + shell_line("pnpm", &test_args), + cwd.display() + ) + })?; + ensure_success(&cwd, "pnpm", &test_args, &output)?; let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; @@ -87,37 +127,6 @@ fn run() -> Result<()> { Ok(()) } -fn run_command(program: &str, args: &[String], cwd: &Path) -> Result { - let output = Command::new(program) - .args(args) - .current_dir(cwd) - .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - ensure_success(cwd, program, args, &output)?; - Ok(output) -} - -fn run_command_forward(program: &str, args: &[String], cwd: &Path) -> Result<()> { - let status = Command::new(program) - .args(args) - .current_dir(cwd) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line(program, args), cwd.display()))?; - ensure_success( - cwd, - program, - args, - &Output { - status, - stdout: Vec::new(), - stderr: Vec::new(), - }, - ) -} - fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { if output.status.success() { return Ok(()); From 0fbec032938df6e2b7e29f55e87782ce5dab5296 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 1 May 2026 17:21:23 -0700 Subject: [PATCH 19/36] [bfops/cargo-test-all]: Address harness parsing review --- crates/csharp-tests/tests/csharp.rs | 50 ++++-------- crates/typescript-tests/tests/typescript.rs | 85 +++++++++------------ 2 files changed, 53 insertions(+), 82 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 3d3c0ce6b70..afc1d9a7c8b 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -7,10 +7,9 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, SpacetimeDbGuard, TestCaseResult}; use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, Output, Stdio}; +use std::process::{Command, ExitStatus, Output}; #[derive(Clone, Debug, Default, Parser)] -#[command(disable_help_flag = true)] struct Args { #[arg(long)] filter: Option, @@ -18,28 +17,10 @@ struct Args { #[arg(long, alias = "list-tests")] list: bool, - #[arg(skip)] + #[arg(last = true)] passthrough: Vec, } -impl Args { - fn parse() -> Self { - let mut args = std::env::args().collect::>(); - let passthrough = args - .iter() - .position(|arg| arg == "--") - .map(|index| args.split_off(index + 1)) - .unwrap_or_default(); - if args.last().is_some_and(|arg| arg == "--") { - args.pop(); - } - - let mut parsed = ::parse_from(args); - parsed.passthrough = passthrough; - parsed - } -} - fn main() { if let Err(err) = run() { eprintln!("{err:#}"); @@ -92,9 +73,6 @@ fn run_dotnet_test( let status = Command::new("dotnet") .args(&list_args) .current_dir(cwd) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) .status() .with_context(|| { format!( @@ -103,16 +81,7 @@ fn run_dotnet_test( cwd.display() ) })?; - ensure_success( - cwd, - "dotnet", - &list_args, - &Output { - status, - stdout: Vec::new(), - stderr: Vec::new(), - }, - )?; + ensure_status_success(cwd, "dotnet", &list_args, status)?; return Ok(()); } @@ -235,6 +204,19 @@ fn run_regression_tests(workspace: &Path) -> Result<()> { Ok(()) } +fn ensure_status_success(cwd: &Path, program: &str, args: &[String], status: ExitStatus) -> Result<()> { + if status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}", + cwd.display(), + shell_line(program, args), + status, + ); +} + fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { if output.status.success() { return Ok(()); diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 016903f630f..f670f43892c 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -7,10 +7,9 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, TestCaseResult}; use std::fs; use std::path::Path; -use std::process::{Command, Output, Stdio}; +use std::process::{Command, ExitStatus, Output}; #[derive(Clone, Debug, Default, Parser)] -#[command(disable_help_flag = true)] struct Args { #[arg(long)] filter: Option, @@ -18,28 +17,10 @@ struct Args { #[arg(long, alias = "list-tests")] list: bool, - #[arg(skip)] + #[arg(last = true)] passthrough: Vec, } -impl Args { - fn parse() -> Self { - let mut args = std::env::args().collect::>(); - let passthrough = args - .iter() - .position(|arg| arg == "--") - .map(|index| args.split_off(index + 1)) - .unwrap_or_default(); - if args.last().is_some_and(|arg| arg == "--") { - args.pop(); - } - - let mut parsed = ::parse_from(args); - parsed.passthrough = passthrough; - parsed - } -} - fn main() { if let Err(err) = run() { eprintln!("{err:#}"); @@ -57,35 +38,30 @@ fn run() -> Result<()> { let report = out_dir.join("vitest.junit.xml"); if args.list { - let mut cmd = vec!["vitest".to_string(), "list".to_string()]; - if let Some(filter) = args.filter { - cmd.push(filter); - } - let status = Command::new("pnpm") - .args(&cmd) - .current_dir(&cwd) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line("pnpm", &cmd), cwd.display()))?; - ensure_success( - &cwd, - "pnpm", - &cmd, - &Output { - status, - stdout: Vec::new(), - stderr: Vec::new(), - }, - )?; - return Ok(()); + return list_tests(&cwd, args.filter); + } + + run_tests(&cwd, &report, args) +} + +fn list_tests(cwd: &Path, filter: Option) -> Result<()> { + let mut cmd = vec!["vitest".to_string(), "list".to_string()]; + if let Some(filter) = filter { + cmd.push(filter); } + let status = Command::new("pnpm") + .args(&cmd) + .current_dir(cwd) + .status() + .with_context(|| format!("failed to spawn `{}` in {}", shell_line("pnpm", &cmd), cwd.display()))?; + ensure_status_success(cwd, "pnpm", &cmd, status) +} +fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { let build_args = ["build".to_string()]; let output = Command::new("pnpm") .args(&build_args) - .current_dir(&cwd) + .current_dir(cwd) .output() .with_context(|| { format!( @@ -94,7 +70,7 @@ fn run() -> Result<()> { cwd.display() ) })?; - ensure_success(&cwd, "pnpm", &build_args, &output)?; + ensure_success(cwd, "pnpm", &build_args, &output)?; let mut test_args = vec![ "test".to_string(), @@ -110,7 +86,7 @@ fn run() -> Result<()> { test_args.extend(args.passthrough); let output = Command::new("pnpm") .args(&test_args) - .current_dir(&cwd) + .current_dir(cwd) .output() .with_context(|| { format!( @@ -119,7 +95,7 @@ fn run() -> Result<()> { cwd.display() ) })?; - ensure_success(&cwd, "pnpm", &test_args, &output)?; + ensure_success(cwd, "pnpm", &test_args, &output)?; let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; @@ -127,6 +103,19 @@ fn run() -> Result<()> { Ok(()) } +fn ensure_status_success(cwd: &Path, program: &str, args: &[String], status: ExitStatus) -> Result<()> { + if status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}", + cwd.display(), + shell_line(program, args), + status, + ); +} + fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { if output.status.success() { return Ok(()); From aabc71a00562c48e15ee95e412d98eb7fdf84be3 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:14:55 -0700 Subject: [PATCH 20/36] [bfops/cargo-test-all]: Address C# harness review --- crates/csharp-tests/tests/csharp.rs | 136 +++++++++----------- crates/typescript-tests/tests/typescript.rs | 53 +++----- tools/ci/src/main.rs | 2 +- 3 files changed, 79 insertions(+), 112 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index afc1d9a7c8b..8be490149cb 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -7,7 +7,7 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, SpacetimeDbGuard, TestCaseResult}; use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, ExitStatus, Output}; +use std::process::{Command, Output}; #[derive(Clone, Debug, Default, Parser)] struct Args { @@ -35,25 +35,19 @@ fn run() -> Result<()> { let out_dir = target_dir().join("csharp-tests"); fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; - run_sdk_tests(&workspace, &out_dir, &args)?; - if !args.list { - run_regression_tests(&workspace)?; - } - - Ok(()) -} - -fn run_sdk_tests(workspace: &Path, out_dir: &Path, args: &Args) -> Result<()> { - prepare_csharp_sdk_solution(workspace)?; - let cwd = workspace.join("sdks/csharp"); + prepare_csharp_sdk_solution(&workspace)?; run_dotnet_test( "csharp sdk", - &cwd, - out_dir, + &workspace.join("sdks/csharp"), + &out_dir, "sdk.trx", - args, + &args, &["-warnaserror".to_string(), "--no-restore".to_string()], )?; + if !args.list { + run_regression_tests(&workspace)?; + } + Ok(()) } @@ -70,18 +64,15 @@ fn run_dotnet_test( if args.list { let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; list_args.extend(extra_args.iter().cloned()); - let status = Command::new("dotnet") + let command_line = shell_line("dotnet", &list_args); + let output = Command::new("dotnet") .args(&list_args) .current_dir(cwd) - .status() - .with_context(|| { - format!( - "failed to spawn `{}` in {}", - shell_line("dotnet", &list_args), - cwd.display() - ) - })?; - ensure_status_success(cwd, "dotnet", &list_args, status)?; + .output() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); return Ok(()); } @@ -100,18 +91,13 @@ fn run_dotnet_test( } test_args.extend(args.passthrough.iter().cloned()); + let command_line = shell_line("dotnet", &test_args); let output = Command::new("dotnet") .args(&test_args) .current_dir(cwd) .output() - .with_context(|| { - format!( - "failed to spawn `{}` in {}", - shell_line("dotnet", &test_args), - cwd.display() - ) - })?; - ensure_success(cwd, "dotnet", &test_args, &output)?; + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, &output)?; let actual_report = find_trx(&report, cwd).with_context(|| format!("failed to locate TRX report for {suite}"))?; let results = parse_trx(&actual_report).with_context(|| format!("failed to parse {suite} TRX report"))?; print_results(suite, &actual_report, &results)?; @@ -138,86 +124,86 @@ fn find_trx(preferred: &Path, cwd: &Path) -> Result { } fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { - let args = ["pack".to_string(), "crates/bindings-csharp/BSATN.Runtime".to_string()]; let output = Command::new("dotnet") - .args(&args) + .args(["pack", "crates/bindings-csharp/BSATN.Runtime"]) .current_dir(workspace) .output() .with_context(|| { format!( - "failed to spawn `{}` in {}", - shell_line("dotnet", &args), + "failed to spawn `dotnet pack crates/bindings-csharp/BSATN.Runtime` in {}", workspace.display() ) })?; - ensure_success(workspace, "dotnet", &args, &output)?; + ensure_success(workspace, "dotnet pack crates/bindings-csharp/BSATN.Runtime", &output)?; - let args = ["pack".to_string(), "crates/bindings-csharp/Runtime".to_string()]; let output = Command::new("dotnet") - .args(&args) + .args(["pack", "crates/bindings-csharp/Runtime"]) .current_dir(workspace) .output() .with_context(|| { format!( - "failed to spawn `{}` in {}", - shell_line("dotnet", &args), + "failed to spawn `dotnet pack crates/bindings-csharp/Runtime` in {}", workspace.display() ) })?; - ensure_success(workspace, "dotnet", &args, &output)?; + ensure_success(workspace, "dotnet pack crates/bindings-csharp/Runtime", &output)?; let cwd = workspace.join("sdks/csharp"); - let args = ["./tools~/write-nuget-config.sh".to_string(), "../..".to_string()]; + // Write out the NuGet config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository + // to be aware of the local versions of the `bindings-csharp` packages in SpacetimeDB, and use them if + // available. Otherwise, `spacetimedb-csharp-sdk` will use the NuGet versions of the packages. + // This means that (if version numbers match) we will test the local versions of the C# packages, even + // if they're not pushed to NuGet. + // See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file. let output = Command::new("bash") - .args(&args) + .args(["./tools~/write-nuget-config.sh", "../.."]) .current_dir(&cwd) .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line("bash", &args), cwd.display()))?; - ensure_success(&cwd, "bash", &args, &output)?; + .with_context(|| { + format!( + "failed to spawn `bash ./tools~/write-nuget-config.sh ../..` in {}", + cwd.display() + ) + })?; + ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", &output)?; - let args = [ - "restore".to_string(), - "--configfile".to_string(), - "NuGet.Config".to_string(), - "SpacetimeDB.ClientSDK.sln".to_string(), - ]; let output = Command::new("dotnet") - .args(&args) + .args(["restore", "--configfile", "NuGet.Config", "SpacetimeDB.ClientSDK.sln"]) .current_dir(&cwd) .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line("dotnet", &args), cwd.display()))?; - ensure_success(&cwd, "dotnet", &args, &output)?; + .with_context(|| { + format!( + "failed to spawn `dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln` in {}", + cwd.display() + ) + })?; + ensure_success( + &cwd, + "dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln", + &output, + )?; Ok(()) } fn run_regression_tests(workspace: &Path) -> Result<()> { let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); let cwd = workspace.join("sdks/csharp"); - let args = ["tools~/run-regression-tests.sh".to_string()]; let output = Command::new("bash") - .args(&args) + .args(["tools~/run-regression-tests.sh"]) .current_dir(&cwd) .env("SPACETIMEDB_SERVER_URL", &guard.host_url) .output() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line("bash", &args), cwd.display()))?; - ensure_success(&cwd, "bash", &args, &output)?; + .with_context(|| { + format!( + "failed to spawn `bash tools~/run-regression-tests.sh` in {}", + cwd.display() + ) + })?; + ensure_success(&cwd, "bash tools~/run-regression-tests.sh", &output)?; Ok(()) } -fn ensure_status_success(cwd: &Path, program: &str, args: &[String], status: ExitStatus) -> Result<()> { - if status.success() { - return Ok(()); - } - - bail!( - "command failed in {}:\n {}\nstatus: {}", - cwd.display(), - shell_line(program, args), - status, - ); -} - -fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { +fn ensure_success(cwd: &Path, command_line: &str, output: &Output) -> Result<()> { if output.status.success() { return Ok(()); } @@ -225,7 +211,7 @@ fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) - bail!( "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", cwd.display(), - shell_line(program, args), + command_line, output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index f670f43892c..664ecb28b8f 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -7,7 +7,7 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, TestCaseResult}; use std::fs; use std::path::Path; -use std::process::{Command, ExitStatus, Output}; +use std::process::{Command, Output}; #[derive(Clone, Debug, Default, Parser)] struct Args { @@ -49,28 +49,27 @@ fn list_tests(cwd: &Path, filter: Option) -> Result<()> { if let Some(filter) = filter { cmd.push(filter); } - let status = Command::new("pnpm") + let command_line = shell_line("pnpm", &cmd); + let output = Command::new("pnpm") .args(&cmd) .current_dir(cwd) - .status() - .with_context(|| format!("failed to spawn `{}` in {}", shell_line("pnpm", &cmd), cwd.display()))?; - ensure_status_success(cwd, "pnpm", &cmd, status) + .output() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + Ok(()) } fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { let build_args = ["build".to_string()]; + let command_line = shell_line("pnpm", &build_args); let output = Command::new("pnpm") .args(&build_args) .current_dir(cwd) .output() - .with_context(|| { - format!( - "failed to spawn `{}` in {}", - shell_line("pnpm", &build_args), - cwd.display() - ) - })?; - ensure_success(cwd, "pnpm", &build_args, &output)?; + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, &output)?; let mut test_args = vec![ "test".to_string(), @@ -84,18 +83,13 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { test_args.push(filter); } test_args.extend(args.passthrough); + let command_line = shell_line("pnpm", &test_args); let output = Command::new("pnpm") .args(&test_args) .current_dir(cwd) .output() - .with_context(|| { - format!( - "failed to spawn `{}` in {}", - shell_line("pnpm", &test_args), - cwd.display() - ) - })?; - ensure_success(cwd, "pnpm", &test_args, &output)?; + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, &output)?; let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; @@ -103,20 +97,7 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { Ok(()) } -fn ensure_status_success(cwd: &Path, program: &str, args: &[String], status: ExitStatus) -> Result<()> { - if status.success() { - return Ok(()); - } - - bail!( - "command failed in {}:\n {}\nstatus: {}", - cwd.display(), - shell_line(program, args), - status, - ); -} - -fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) -> Result<()> { +fn ensure_success(cwd: &Path, command_line: &str, output: &Output) -> Result<()> { if output.status.success() { return Ok(()); } @@ -124,7 +105,7 @@ fn ensure_success(cwd: &Path, program: &str, args: &[String], output: &Output) - bail!( "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", cwd.display(), - shell_line(program, args), + command_line, output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 843ddf6009a..4f9b5e5b655 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -459,7 +459,7 @@ fn run_csharp_tests() -> Result<()> { "spacetimedb-standalone/allow_loopback_http_for_tests" ) .run()?; - cmd!("cargo", "test", "-p", "spacetimedb-csharp-tests", "--test", "csharp").run()?; + cmd!("cargo", "test", "-p", "spacetimedb-csharp-tests").run()?; cmd!( "dotnet", "format", From 452f7eb7f9e064e168fc1f8bdf4adaeaaccd94c3 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:24:51 -0700 Subject: [PATCH 21/36] [bfops/cargo-test-all]: Address harness output review --- crates/csharp-tests/tests/csharp.rs | 42 ++++++++++++++------- crates/typescript-tests/tests/typescript.rs | 22 ++++++----- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index 8be490149cb..cc721792bb6 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -7,7 +7,7 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, SpacetimeDbGuard, TestCaseResult}; use std::fs; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::{Command, ExitStatus}; #[derive(Clone, Debug, Default, Parser)] struct Args { @@ -70,9 +70,9 @@ fn run_dotnet_test( .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, &output)?; print!("{}", String::from_utf8_lossy(&output.stdout)); eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(cwd, &command_line, output.status)?; return Ok(()); } @@ -97,7 +97,9 @@ fn run_dotnet_test( .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(cwd, &command_line, output.status)?; let actual_report = find_trx(&report, cwd).with_context(|| format!("failed to locate TRX report for {suite}"))?; let results = parse_trx(&actual_report).with_context(|| format!("failed to parse {suite} TRX report"))?; print_results(suite, &actual_report, &results)?; @@ -134,7 +136,13 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { workspace.display() ) })?; - ensure_success(workspace, "dotnet pack crates/bindings-csharp/BSATN.Runtime", &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success( + workspace, + "dotnet pack crates/bindings-csharp/BSATN.Runtime", + output.status, + )?; let output = Command::new("dotnet") .args(["pack", "crates/bindings-csharp/Runtime"]) @@ -146,7 +154,9 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { workspace.display() ) })?; - ensure_success(workspace, "dotnet pack crates/bindings-csharp/Runtime", &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(workspace, "dotnet pack crates/bindings-csharp/Runtime", output.status)?; let cwd = workspace.join("sdks/csharp"); // Write out the NuGet config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository @@ -165,7 +175,9 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { cwd.display() ) })?; - ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", output.status)?; let output = Command::new("dotnet") .args(["restore", "--configfile", "NuGet.Config", "SpacetimeDB.ClientSDK.sln"]) @@ -177,10 +189,12 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { cwd.display() ) })?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); ensure_success( &cwd, "dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln", - &output, + output.status, )?; Ok(()) } @@ -199,22 +213,22 @@ fn run_regression_tests(workspace: &Path) -> Result<()> { cwd.display() ) })?; - ensure_success(&cwd, "bash tools~/run-regression-tests.sh", &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(&cwd, "bash tools~/run-regression-tests.sh", output.status)?; Ok(()) } -fn ensure_success(cwd: &Path, command_line: &str, output: &Output) -> Result<()> { - if output.status.success() { +fn ensure_success(cwd: &Path, command_line: &str, status: ExitStatus) -> Result<()> { + if status.success() { return Ok(()); } bail!( - "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + "command failed in {}:\n {}\nstatus: {}", cwd.display(), command_line, - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + status ); } diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 664ecb28b8f..7850684bed8 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -7,7 +7,7 @@ use quick_xml::Reader; use spacetimedb_language_test_support::{print_results, target_dir, Outcome, TestCaseResult}; use std::fs; use std::path::Path; -use std::process::{Command, Output}; +use std::process::{Command, ExitStatus}; #[derive(Clone, Debug, Default, Parser)] struct Args { @@ -55,9 +55,9 @@ fn list_tests(cwd: &Path, filter: Option) -> Result<()> { .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, &output)?; print!("{}", String::from_utf8_lossy(&output.stdout)); eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(cwd, &command_line, output.status)?; Ok(()) } @@ -69,7 +69,9 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(cwd, &command_line, output.status)?; let mut test_args = vec![ "test".to_string(), @@ -89,7 +91,9 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { .current_dir(cwd) .output() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, &output)?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + ensure_success(cwd, &command_line, output.status)?; let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; @@ -97,18 +101,16 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { Ok(()) } -fn ensure_success(cwd: &Path, command_line: &str, output: &Output) -> Result<()> { - if output.status.success() { +fn ensure_success(cwd: &Path, command_line: &str, status: ExitStatus) -> Result<()> { + if status.success() { return Ok(()); } bail!( - "command failed in {}:\n {}\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + "command failed in {}:\n {}\nstatus: {}", cwd.display(), command_line, - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + status ); } From 6f2877eba08ca61083b3367efabf8ec86feef9df Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:30:51 -0700 Subject: [PATCH 22/36] [bfops/cargo-test-all]: Stream harness command output --- crates/csharp-tests/tests/csharp.rs | 60 ++++++++------------- crates/typescript-tests/tests/typescript.rs | 24 ++++----- 2 files changed, 30 insertions(+), 54 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index cc721792bb6..df2b0f7e67d 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -65,14 +65,12 @@ fn run_dotnet_test( let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; list_args.extend(extra_args.iter().cloned()); let command_line = shell_line("dotnet", &list_args); - let output = Command::new("dotnet") + let status = Command::new("dotnet") .args(&list_args) .current_dir(cwd) - .output() + .status() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(cwd, &command_line, output.status)?; + ensure_success(cwd, &command_line, status)?; return Ok(()); } @@ -92,14 +90,12 @@ fn run_dotnet_test( test_args.extend(args.passthrough.iter().cloned()); let command_line = shell_line("dotnet", &test_args); - let output = Command::new("dotnet") + let status = Command::new("dotnet") .args(&test_args) .current_dir(cwd) - .output() + .status() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(cwd, &command_line, output.status)?; + ensure_success(cwd, &command_line, status)?; let actual_report = find_trx(&report, cwd).with_context(|| format!("failed to locate TRX report for {suite}"))?; let results = parse_trx(&actual_report).with_context(|| format!("failed to parse {suite} TRX report"))?; print_results(suite, &actual_report, &results)?; @@ -126,37 +122,29 @@ fn find_trx(preferred: &Path, cwd: &Path) -> Result { } fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { - let output = Command::new("dotnet") + let status = Command::new("dotnet") .args(["pack", "crates/bindings-csharp/BSATN.Runtime"]) .current_dir(workspace) - .output() + .status() .with_context(|| { format!( "failed to spawn `dotnet pack crates/bindings-csharp/BSATN.Runtime` in {}", workspace.display() ) })?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success( - workspace, - "dotnet pack crates/bindings-csharp/BSATN.Runtime", - output.status, - )?; + ensure_success(workspace, "dotnet pack crates/bindings-csharp/BSATN.Runtime", status)?; - let output = Command::new("dotnet") + let status = Command::new("dotnet") .args(["pack", "crates/bindings-csharp/Runtime"]) .current_dir(workspace) - .output() + .status() .with_context(|| { format!( "failed to spawn `dotnet pack crates/bindings-csharp/Runtime` in {}", workspace.display() ) })?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(workspace, "dotnet pack crates/bindings-csharp/Runtime", output.status)?; + ensure_success(workspace, "dotnet pack crates/bindings-csharp/Runtime", status)?; let cwd = workspace.join("sdks/csharp"); // Write out the NuGet config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository @@ -165,36 +153,32 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { // This means that (if version numbers match) we will test the local versions of the C# packages, even // if they're not pushed to NuGet. // See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file. - let output = Command::new("bash") + let status = Command::new("bash") .args(["./tools~/write-nuget-config.sh", "../.."]) .current_dir(&cwd) - .output() + .status() .with_context(|| { format!( "failed to spawn `bash ./tools~/write-nuget-config.sh ../..` in {}", cwd.display() ) })?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", output.status)?; + ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", status)?; - let output = Command::new("dotnet") + let status = Command::new("dotnet") .args(["restore", "--configfile", "NuGet.Config", "SpacetimeDB.ClientSDK.sln"]) .current_dir(&cwd) - .output() + .status() .with_context(|| { format!( "failed to spawn `dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln` in {}", cwd.display() ) })?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); ensure_success( &cwd, "dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln", - output.status, + status, )?; Ok(()) } @@ -202,20 +186,18 @@ fn prepare_csharp_sdk_solution(workspace: &Path) -> Result<()> { fn run_regression_tests(workspace: &Path) -> Result<()> { let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); let cwd = workspace.join("sdks/csharp"); - let output = Command::new("bash") + let status = Command::new("bash") .args(["tools~/run-regression-tests.sh"]) .current_dir(&cwd) .env("SPACETIMEDB_SERVER_URL", &guard.host_url) - .output() + .status() .with_context(|| { format!( "failed to spawn `bash tools~/run-regression-tests.sh` in {}", cwd.display() ) })?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(&cwd, "bash tools~/run-regression-tests.sh", output.status)?; + ensure_success(&cwd, "bash tools~/run-regression-tests.sh", status)?; Ok(()) } diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 7850684bed8..c56009f838e 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -50,28 +50,24 @@ fn list_tests(cwd: &Path, filter: Option) -> Result<()> { cmd.push(filter); } let command_line = shell_line("pnpm", &cmd); - let output = Command::new("pnpm") + let status = Command::new("pnpm") .args(&cmd) .current_dir(cwd) - .output() + .status() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(cwd, &command_line, output.status)?; + ensure_success(cwd, &command_line, status)?; Ok(()) } fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { let build_args = ["build".to_string()]; let command_line = shell_line("pnpm", &build_args); - let output = Command::new("pnpm") + let status = Command::new("pnpm") .args(&build_args) .current_dir(cwd) - .output() + .status() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(cwd, &command_line, output.status)?; + ensure_success(cwd, &command_line, status)?; let mut test_args = vec![ "test".to_string(), @@ -86,14 +82,12 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { } test_args.extend(args.passthrough); let command_line = shell_line("pnpm", &test_args); - let output = Command::new("pnpm") + let status = Command::new("pnpm") .args(&test_args) .current_dir(cwd) - .output() + .status() .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - ensure_success(cwd, &command_line, output.status)?; + ensure_success(cwd, &command_line, status)?; let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; print_results("typescript", &report, &results)?; From b11eda9e0c4c15967abe183d28edebf6651204ff Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:33:52 -0700 Subject: [PATCH 23/36] [bfops/cargo-test-all]: Move C# list dispatch to harness entrypoint --- crates/csharp-tests/tests/csharp.rs | 37 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index df2b0f7e67d..e31ba72c188 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -36,6 +36,14 @@ fn run() -> Result<()> { fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; prepare_csharp_sdk_solution(&workspace)?; + if args.list { + list_dotnet_tests( + &workspace.join("sdks/csharp"), + &["-warnaserror".to_string(), "--no-restore".to_string()], + )?; + return Ok(()); + } + run_dotnet_test( "csharp sdk", &workspace.join("sdks/csharp"), @@ -44,13 +52,23 @@ fn run() -> Result<()> { &args, &["-warnaserror".to_string(), "--no-restore".to_string()], )?; - if !args.list { - run_regression_tests(&workspace)?; - } + run_regression_tests(&workspace)?; Ok(()) } +fn list_dotnet_tests(cwd: &Path, extra_args: &[String]) -> Result<()> { + let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; + list_args.extend(extra_args.iter().cloned()); + let command_line = shell_line("dotnet", &list_args); + let status = Command::new("dotnet") + .args(&list_args) + .current_dir(cwd) + .status() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, status) +} + fn run_dotnet_test( suite: &str, cwd: &Path, @@ -61,19 +79,6 @@ fn run_dotnet_test( ) -> Result<()> { let report = out_dir.join(report_name); - if args.list { - let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; - list_args.extend(extra_args.iter().cloned()); - let command_line = shell_line("dotnet", &list_args); - let status = Command::new("dotnet") - .args(&list_args) - .current_dir(cwd) - .status() - .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; - ensure_success(cwd, &command_line, status)?; - return Ok(()); - } - let mut test_args = vec![ "test".to_string(), "-warnaserror".to_string(), From 8b7c00a0823911752142c0b00999f0a5ee838163 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:37:31 -0700 Subject: [PATCH 24/36] [bfops/cargo-test-all]: Inline C# harness SDK args --- crates/csharp-tests/tests/csharp.rs | 35 +++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index e31ba72c188..a553c9e101b 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -37,29 +37,23 @@ fn run() -> Result<()> { prepare_csharp_sdk_solution(&workspace)?; if args.list { - list_dotnet_tests( - &workspace.join("sdks/csharp"), - &["-warnaserror".to_string(), "--no-restore".to_string()], - )?; + list_dotnet_tests(&workspace.join("sdks/csharp"))?; return Ok(()); } - run_dotnet_test( - "csharp sdk", - &workspace.join("sdks/csharp"), - &out_dir, - "sdk.trx", - &args, - &["-warnaserror".to_string(), "--no-restore".to_string()], - )?; + run_dotnet_test("csharp sdk", &workspace.join("sdks/csharp"), &out_dir, "sdk.trx", &args)?; run_regression_tests(&workspace)?; Ok(()) } -fn list_dotnet_tests(cwd: &Path, extra_args: &[String]) -> Result<()> { - let mut list_args = vec!["test".to_string(), "--list-tests".to_string()]; - list_args.extend(extra_args.iter().cloned()); +fn list_dotnet_tests(cwd: &Path) -> Result<()> { + let list_args = [ + "test".to_string(), + "--list-tests".to_string(), + "-warnaserror".to_string(), + "--no-restore".to_string(), + ]; let command_line = shell_line("dotnet", &list_args); let status = Command::new("dotnet") .args(&list_args) @@ -69,14 +63,7 @@ fn list_dotnet_tests(cwd: &Path, extra_args: &[String]) -> Result<()> { ensure_success(cwd, &command_line, status) } -fn run_dotnet_test( - suite: &str, - cwd: &Path, - out_dir: &Path, - report_name: &str, - args: &Args, - extra_args: &[String], -) -> Result<()> { +fn run_dotnet_test(suite: &str, cwd: &Path, out_dir: &Path, report_name: &str, args: &Args) -> Result<()> { let report = out_dir.join(report_name); let mut test_args = vec![ @@ -86,8 +73,8 @@ fn run_dotnet_test( out_dir.display().to_string(), "--logger".to_string(), format!("trx;LogFileName={report_name}"), + "--no-restore".to_string(), ]; - test_args.extend(extra_args.iter().filter(|arg| arg.as_str() != "-warnaserror").cloned()); if let Some(filter) = &args.filter { test_args.push("--filter".to_string()); test_args.push(filter.clone()); From 0150d6a7cf15772c4de7033131a0866b510fc575 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 12:41:07 -0700 Subject: [PATCH 25/36] [bfops/cargo-test-all]: Pass harness args explicitly --- crates/csharp-tests/tests/csharp.rs | 24 ++++++++++++++++----- crates/typescript-tests/tests/typescript.rs | 8 +++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/csharp-tests/tests/csharp.rs b/crates/csharp-tests/tests/csharp.rs index a553c9e101b..8d415d14f0e 100644 --- a/crates/csharp-tests/tests/csharp.rs +++ b/crates/csharp-tests/tests/csharp.rs @@ -41,7 +41,14 @@ fn run() -> Result<()> { return Ok(()); } - run_dotnet_test("csharp sdk", &workspace.join("sdks/csharp"), &out_dir, "sdk.trx", &args)?; + run_dotnet_test( + "csharp sdk", + &workspace.join("sdks/csharp"), + &out_dir, + "sdk.trx", + args.filter.as_deref(), + &args.passthrough, + )?; run_regression_tests(&workspace)?; Ok(()) @@ -63,7 +70,14 @@ fn list_dotnet_tests(cwd: &Path) -> Result<()> { ensure_success(cwd, &command_line, status) } -fn run_dotnet_test(suite: &str, cwd: &Path, out_dir: &Path, report_name: &str, args: &Args) -> Result<()> { +fn run_dotnet_test( + suite: &str, + cwd: &Path, + out_dir: &Path, + report_name: &str, + filter: Option<&str>, + passthrough: &[String], +) -> Result<()> { let report = out_dir.join(report_name); let mut test_args = vec![ @@ -75,11 +89,11 @@ fn run_dotnet_test(suite: &str, cwd: &Path, out_dir: &Path, report_name: &str, a format!("trx;LogFileName={report_name}"), "--no-restore".to_string(), ]; - if let Some(filter) = &args.filter { + if let Some(filter) = filter { test_args.push("--filter".to_string()); - test_args.push(filter.clone()); + test_args.push(filter.to_string()); } - test_args.extend(args.passthrough.iter().cloned()); + test_args.extend(passthrough.iter().cloned()); let command_line = shell_line("dotnet", &test_args); let status = Command::new("dotnet") diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index c56009f838e..79f30845a87 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -41,7 +41,7 @@ fn run() -> Result<()> { return list_tests(&cwd, args.filter); } - run_tests(&cwd, &report, args) + run_tests(&cwd, &report, args.filter, args.passthrough) } fn list_tests(cwd: &Path, filter: Option) -> Result<()> { @@ -59,7 +59,7 @@ fn list_tests(cwd: &Path, filter: Option) -> Result<()> { Ok(()) } -fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { +fn run_tests(cwd: &Path, report: &Path, filter: Option, passthrough: Vec) -> Result<()> { let build_args = ["build".to_string()]; let command_line = shell_line("pnpm", &build_args); let status = Command::new("pnpm") @@ -76,11 +76,11 @@ fn run_tests(cwd: &Path, report: &Path, args: Args) -> Result<()> { "--reporter=junit".to_string(), format!("--outputFile={}", report.display()), ]; - if let Some(filter) = args.filter { + if let Some(filter) = filter { test_args.push("-t".to_string()); test_args.push(filter); } - test_args.extend(args.passthrough); + test_args.extend(passthrough); let command_line = shell_line("pnpm", &test_args); let status = Command::new("pnpm") .args(&test_args) From 339a3f45f0557f5f8b4a3210461e78091a4ecada Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 14:03:32 -0700 Subject: [PATCH 26/36] [bfops/cargo-test-all]: fix --- crates/typescript-tests/tests/typescript.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typescript-tests/tests/typescript.rs b/crates/typescript-tests/tests/typescript.rs index 79f30845a87..64671728a12 100644 --- a/crates/typescript-tests/tests/typescript.rs +++ b/crates/typescript-tests/tests/typescript.rs @@ -89,8 +89,8 @@ fn run_tests(cwd: &Path, report: &Path, filter: Option, passthrough: Vec .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; ensure_success(cwd, &command_line, status)?; - let results = parse_junit(&report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; - print_results("typescript", &report, &results)?; + let results = parse_junit(report).with_context(|| "failed to parse TypeScript Vitest JUnit report")?; + print_results("typescript", report, &results)?; Ok(()) } From 50b7bd770dc0fb16e56dff77d70ab82f26fc195c Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 17:02:33 -0700 Subject: [PATCH 27/36] [bfops/cargo-test-all]: move `dotnet test` --- tools/ci/src/main.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 4f9b5e5b655..08a1293a68d 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -447,6 +447,9 @@ fn run_typescript_tests() -> Result<()> { } fn run_csharp_tests() -> Result<()> { + cmd!("dotnet", "test", "-warnaserror") + .dir("crates/bindings-csharp") + .run()?; cmd!( "cargo", "build", @@ -459,6 +462,9 @@ fn run_csharp_tests() -> Result<()> { "spacetimedb-standalone/allow_loopback_http_for_tests" ) .run()?; + cmd!("dotnet", "test", "-warnaserror") + .dir("crates/bindings-csharp") + .run()?; cmd!("cargo", "test", "-p", "spacetimedb-csharp-tests").run()?; cmd!( "dotnet", @@ -573,9 +579,6 @@ fn main() -> Result<()> { ) .run()?; cmd!("bash", "tools/check-diff.sh", "crates/bindings-csharp").run()?; - cmd!("dotnet", "test", "-warnaserror") - .dir("crates/bindings-csharp") - .run()?; } Some(CiCmd::Lint) => { From bb7a75c2c48d0c4e7939852838c7a3da9de96300 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 18:04:08 -0700 Subject: [PATCH 28/36] [bfops/cargo-test-all]: remove --- tools/ci/src/main.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 08a1293a68d..6d731c3bec9 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -462,9 +462,6 @@ fn run_csharp_tests() -> Result<()> { "spacetimedb-standalone/allow_loopback_http_for_tests" ) .run()?; - cmd!("dotnet", "test", "-warnaserror") - .dir("crates/bindings-csharp") - .run()?; cmd!("cargo", "test", "-p", "spacetimedb-csharp-tests").run()?; cmd!( "dotnet", From b1e865a6a2073283aab2d0f33c6b8faaee414643 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 20:03:07 -0700 Subject: [PATCH 29/36] [bfops/cargo-test-all]: generate --- .../procedure-client/src/module_bindings/mod.rs | 2 +- .../module_bindings/read_my_schema_procedure.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index 18eef1b125e..d4a5a2da02c 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.5 (commit ca7484e072f9514fb2f890f26600a5d096f59431). +// This was generated using spacetimedb cli version 2.2.0 (commit bb7a75c2c48d0c4e7939852838c7a3da9de96300). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs index eaab6c7626f..d5b63873401 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs @@ -6,7 +6,9 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -struct ReadMySchemaArgs {} +struct ReadMySchemaArgs { + pub server_url: String, +} impl __sdk::InModule for ReadMySchemaArgs { type Module = super::RemoteModule; @@ -17,12 +19,13 @@ impl __sdk::InModule for ReadMySchemaArgs { /// /// Implemented for [`super::RemoteProcedures`]. pub trait read_my_schema { - fn read_my_schema(&self) { - self.read_my_schema_then(|_, _| {}); + fn read_my_schema(&self, server_url: String) { + self.read_my_schema_then(server_url, |_, _| {}); } fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ); @@ -31,10 +34,14 @@ pub trait read_my_schema { impl read_my_schema for super::RemoteProcedures { fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ) { - self.imp - .invoke_procedure_with_callback::<_, String>("read_my_schema", ReadMySchemaArgs {}, __callback); + self.imp.invoke_procedure_with_callback::<_, String>( + "read_my_schema", + ReadMySchemaArgs { server_url }, + __callback, + ); } } From 51a5eb87ae2a2aa74d980501e85c4f1db1d57d8d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 10:18:08 -0700 Subject: [PATCH 30/36] [bfops/cargo-test-all]: revert --- .../procedure-client/src/module_bindings/mod.rs | 2 +- .../module_bindings/read_my_schema_procedure.rs | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index d4a5a2da02c..18eef1b125e 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.2.0 (commit bb7a75c2c48d0c4e7939852838c7a3da9de96300). +// This was generated using spacetimedb cli version 2.0.5 (commit ca7484e072f9514fb2f890f26600a5d096f59431). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs index d5b63873401..eaab6c7626f 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs @@ -6,9 +6,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -struct ReadMySchemaArgs { - pub server_url: String, -} +struct ReadMySchemaArgs {} impl __sdk::InModule for ReadMySchemaArgs { type Module = super::RemoteModule; @@ -19,13 +17,12 @@ impl __sdk::InModule for ReadMySchemaArgs { /// /// Implemented for [`super::RemoteProcedures`]. pub trait read_my_schema { - fn read_my_schema(&self, server_url: String) { - self.read_my_schema_then(server_url, |_, _| {}); + fn read_my_schema(&self) { + self.read_my_schema_then(|_, _| {}); } fn read_my_schema_then( &self, - server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ); @@ -34,14 +31,10 @@ pub trait read_my_schema { impl read_my_schema for super::RemoteProcedures { fn read_my_schema_then( &self, - server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ) { - self.imp.invoke_procedure_with_callback::<_, String>( - "read_my_schema", - ReadMySchemaArgs { server_url }, - __callback, - ); + self.imp + .invoke_procedure_with_callback::<_, String>("read_my_schema", ReadMySchemaArgs {}, __callback); } } From cfc3fe1b4e9d9d2ea5dce261a561cdc875f533a0 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 10:28:13 -0700 Subject: [PATCH 31/36] [bfops/cargo-test-all]: fix --- .../src/module_bindings/mod.rs | 2 +- .../read_my_schema_procedure.rs | 17 +++++--- .../procedure-client/src/test_handlers.rs | 41 ++++++++++--------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index 18eef1b125e..d4a5a2da02c 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.0.5 (commit ca7484e072f9514fb2f890f26600a5d096f59431). +// This was generated using spacetimedb cli version 2.2.0 (commit bb7a75c2c48d0c4e7939852838c7a3da9de96300). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs index eaab6c7626f..d5b63873401 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/read_my_schema_procedure.rs @@ -6,7 +6,9 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -struct ReadMySchemaArgs {} +struct ReadMySchemaArgs { + pub server_url: String, +} impl __sdk::InModule for ReadMySchemaArgs { type Module = super::RemoteModule; @@ -17,12 +19,13 @@ impl __sdk::InModule for ReadMySchemaArgs { /// /// Implemented for [`super::RemoteProcedures`]. pub trait read_my_schema { - fn read_my_schema(&self) { - self.read_my_schema_then(|_, _| {}); + fn read_my_schema(&self, server_url: String) { + self.read_my_schema_then(server_url, |_, _| {}); } fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ); @@ -31,10 +34,14 @@ pub trait read_my_schema { impl read_my_schema for super::RemoteProcedures { fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ) { - self.imp - .invoke_procedure_with_callback::<_, String>("read_my_schema", ReadMySchemaArgs {}, __callback); + self.imp.invoke_procedure_with_callback::<_, String>( + "read_my_schema", + ReadMySchemaArgs { server_url }, + __callback, + ); } } diff --git a/sdks/rust/tests/procedure-client/src/test_handlers.rs b/sdks/rust/tests/procedure-client/src/test_handlers.rs index d3f75c0698a..3820b9c0733 100644 --- a/sdks/rust/tests/procedure-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedure-client/src/test_handlers.rs @@ -257,26 +257,27 @@ async fn exec_procedure_http_ok(db_name: &str) { let test_counter = test_counter.clone(); move |ctx| { let result = test_counter.add_test("invoke_http"); - ctx.procedures.read_my_schema_then(move |_ctx, res| { - result( - // It's a try block! - #[allow(clippy::redundant_closure_call)] - (|| { - anyhow::ensure!(res.is_ok(), "Expected Ok result but got {res:?}"); - let module_def: RawModuleDefV9 = spacetimedb_lib::de::serde::deserialize_from( - &mut serde_json::Deserializer::from_str(&res.unwrap()), - )?; - anyhow::ensure!(module_def.misc_exports.iter().any(|misc_export| { - if let RawMiscModuleExportV9::Procedure(procedure_def) = misc_export { - &*procedure_def.name == "read_my_schema" - } else { - false - } - })); - Ok(()) - })(), - ) - }) + ctx.procedures + .read_my_schema_then(LOCALHOST.to_owned(), move |_ctx, res| { + result( + // It's a try block! + #[allow(clippy::redundant_closure_call)] + (|| { + anyhow::ensure!(res.is_ok(), "Expected Ok result but got {res:?}"); + let module_def: RawModuleDefV9 = spacetimedb_lib::de::serde::deserialize_from( + &mut serde_json::Deserializer::from_str(&res.unwrap()), + )?; + anyhow::ensure!(module_def.misc_exports.iter().any(|misc_export| { + if let RawMiscModuleExportV9::Procedure(procedure_def) = misc_export { + &*procedure_def.name == "read_my_schema" + } else { + false + } + })); + Ok(()) + })(), + ) + }) } }) .await; From 3c9813b5a17be2607bdb4e34c582fddf807c53f5 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 10:43:42 -0700 Subject: [PATCH 32/36] [bfops/cargo-test-all]: update --- sdks/rust/tests/procedure-client/src/module_bindings/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs index d4a5a2da02c..239bb5a9d37 100644 --- a/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs +++ b/sdks/rust/tests/procedure-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.2.0 (commit bb7a75c2c48d0c4e7939852838c7a3da9de96300). +// This was generated using spacetimedb cli version 2.2.0 (commit cfc3fe1b4e9d9d2ea5dce261a561cdc875f533a0). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; From 6b1c077d097227cc3aa81126f1a0a6dde84e59ca Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 10:52:49 -0700 Subject: [PATCH 33/36] [bfops/argument-null-exception]: Fix ArgumentNullException --- crates/bindings-csharp/BSATN.Runtime/Builtins.cs | 2 ++ crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index ec2331aa6a8..b324c1d56d7 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -1,6 +1,8 @@ namespace SpacetimeDB; +#if !NET5_0_OR_GREATER using System.Diagnostics; +#endif using System.Runtime.InteropServices; using SpacetimeDB.BSATN; diff --git a/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs b/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs index 45f481847b4..db16bd56cd7 100644 --- a/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs +++ b/crates/bindings-csharp/BSATN.Runtime/QueryBuilder.cs @@ -877,10 +877,14 @@ public static string FormatStringLiteral(ReadOnlySpan value) => public static string FormatHexLiteral(string hex) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(hex); +#else if (hex is null) { throw new ArgumentNullException(nameof(hex)); } +#endif var s = hex; if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) From a46ce8d0f6d4433f2aee85677f27ddfc275a6e03 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 11:04:47 -0700 Subject: [PATCH 34/36] [bfops/argument-null-exception]: format --- crates/bindings-csharp/BSATN.Runtime/Builtins.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs index b324c1d56d7..e204985b54a 100644 --- a/crates/bindings-csharp/BSATN.Runtime/Builtins.cs +++ b/crates/bindings-csharp/BSATN.Runtime/Builtins.cs @@ -1,10 +1,10 @@ namespace SpacetimeDB; +using System.Runtime.InteropServices; +using SpacetimeDB.BSATN; #if !NET5_0_OR_GREATER using System.Diagnostics; #endif -using System.Runtime.InteropServices; -using SpacetimeDB.BSATN; internal static class Util { From 6e1d0c97c95b12f4cb2806fcf6059b86a94e509d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 11:10:58 -0700 Subject: [PATCH 35/36] [bfops/cargo-test-all]: mybe fix? --- tools/ci/src/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 6d731c3bec9..c48dd41c14a 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -447,6 +447,16 @@ fn run_typescript_tests() -> Result<()> { } fn run_csharp_tests() -> Result<()> { + cmd!( + "cargo", + "run", + "-p", + "spacetimedb-codegen", + "--example", + "regen-csharp-moduledef", + ) + .run()?; + cmd!("bash", "tools/check-diff.sh", "crates/bindings-csharp").run()?; cmd!("dotnet", "test", "-warnaserror") .dir("crates/bindings-csharp") .run()?; @@ -566,16 +576,6 @@ fn main() -> Result<()> { ) .run()?; cmd!("bash", "tools/check-diff.sh").run()?; - cmd!( - "cargo", - "run", - "-p", - "spacetimedb-codegen", - "--example", - "regen-csharp-moduledef", - ) - .run()?; - cmd!("bash", "tools/check-diff.sh", "crates/bindings-csharp").run()?; } Some(CiCmd::Lint) => { From e538eceb15ad94e17535717be08e423fcef0ae27 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 11:13:38 -0700 Subject: [PATCH 36/36] [bfops/cargo-test-all]: more server url --- .../sdk-test-procedure-cpp/client/src/main.rs | 3 ++- .../read_my_schema_procedure.rs | 17 ++++++++++++----- modules/sdk-test-procedure-cpp/src/lib.cpp | 4 ++-- modules/sdk-test-procedure-cs/Lib.cs | 6 +++--- modules/sdk-test-procedure-ts/src/index.ts | 18 +++++++++++------- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/modules/sdk-test-procedure-cpp/client/src/main.rs b/modules/sdk-test-procedure-cpp/client/src/main.rs index 752f91d622a..dba675970c7 100644 --- a/modules/sdk-test-procedure-cpp/client/src/main.rs +++ b/modules/sdk-test-procedure-cpp/client/src/main.rs @@ -107,6 +107,7 @@ fn on_sub_error(_ctx: &ErrorContext, err: Error) { /// Run the procedure tests fn run_tests(ctx: &DbConnection) { println!("\n=== Running Procedure Tests ===\n"); + let server_url: String = env::var("SPACETIMEDB_HOST").unwrap_or("http://localhost:3000".to_string()); // Test return_primitive println!("Testing return_primitive(10, 32)..."); @@ -232,7 +233,7 @@ fn run_tests(ctx: &DbConnection) { // Test read_my_schema (HTTP) println!("Testing read_my_schema (HTTP)..."); - ctx.procedures.read_my_schema_then(|_, res| match res { + ctx.procedures.read_my_schema_then(server_url, |_, res| match res { Ok(schema) => { if !schema.is_empty() { println!(" ✓ read_my_schema returned schema data ({} bytes)", schema.len()); diff --git a/modules/sdk-test-procedure-cpp/client/src/module_bindings/read_my_schema_procedure.rs b/modules/sdk-test-procedure-cpp/client/src/module_bindings/read_my_schema_procedure.rs index eaab6c7626f..d5b63873401 100644 --- a/modules/sdk-test-procedure-cpp/client/src/module_bindings/read_my_schema_procedure.rs +++ b/modules/sdk-test-procedure-cpp/client/src/module_bindings/read_my_schema_procedure.rs @@ -6,7 +6,9 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -struct ReadMySchemaArgs {} +struct ReadMySchemaArgs { + pub server_url: String, +} impl __sdk::InModule for ReadMySchemaArgs { type Module = super::RemoteModule; @@ -17,12 +19,13 @@ impl __sdk::InModule for ReadMySchemaArgs { /// /// Implemented for [`super::RemoteProcedures`]. pub trait read_my_schema { - fn read_my_schema(&self) { - self.read_my_schema_then(|_, _| {}); + fn read_my_schema(&self, server_url: String) { + self.read_my_schema_then(server_url, |_, _| {}); } fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ); @@ -31,10 +34,14 @@ pub trait read_my_schema { impl read_my_schema for super::RemoteProcedures { fn read_my_schema_then( &self, + server_url: String, __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, ) { - self.imp - .invoke_procedure_with_callback::<_, String>("read_my_schema", ReadMySchemaArgs {}, __callback); + self.imp.invoke_procedure_with_callback::<_, String>( + "read_my_schema", + ReadMySchemaArgs { server_url }, + __callback, + ); } } diff --git a/modules/sdk-test-procedure-cpp/src/lib.cpp b/modules/sdk-test-procedure-cpp/src/lib.cpp index 31e3669703a..616a030a9dc 100644 --- a/modules/sdk-test-procedure-cpp/src/lib.cpp +++ b/modules/sdk-test-procedure-cpp/src/lib.cpp @@ -140,7 +140,7 @@ SPACETIMEDB_PROCEDURE(Unit, insert_with_tx_rollback, ProcedureContext ctx) { #ifdef SPACETIMEDB_UNSTABLE_FEATURES // Test HTTP GET request to the module's own schema endpoint -SPACETIMEDB_PROCEDURE(std::string, read_my_schema, ProcedureContext ctx) { +SPACETIMEDB_PROCEDURE(std::string, read_my_schema, ProcedureContext ctx, std::string server_url) { // Get the module identity (database address) Identity module_identity = ctx.database_identity(); std::string identity_hex = module_identity.to_hex_string(); @@ -148,7 +148,7 @@ SPACETIMEDB_PROCEDURE(std::string, read_my_schema, ProcedureContext ctx) { LOG_INFO("read_my_schema using identity: " + identity_hex); // Make HTTP GET request to the schema endpoint (matches Rust) - std::string url = "http://localhost:3000/v1/database/" + identity_hex + "/schema?version=9"; + std::string url = server_url + "/v1/database/" + identity_hex + "/schema?version=9"; auto result = ctx.http.get(url); if (!result.is_ok()) { diff --git a/modules/sdk-test-procedure-cs/Lib.cs b/modules/sdk-test-procedure-cs/Lib.cs index 56476a3d18a..d225e48ffc7 100644 --- a/modules/sdk-test-procedure-cs/Lib.cs +++ b/modules/sdk-test-procedure-cs/Lib.cs @@ -66,10 +66,10 @@ public static void WillPanic(ProcedureContext ctx) /// Test HTTP GET request to the module's own schema endpoint /// [SpacetimeDB.Procedure] - public static string ReadMySchema(ProcedureContext ctx) + public static string ReadMySchema(ProcedureContext ctx, string serverUrl) { var moduleIdentity = ProcedureContextBase.Identity; - var result = ctx.Http.Get($"http://localhost:3000/v1/database/{moduleIdentity}/schema?version=9"); + var result = ctx.Http.Get($"{serverUrl}/v1/database/{moduleIdentity}/schema?version=9"); return result.Match( response => response.Body.ToStringUtf8Lossy(), error => throw new Exception($"HTTP request failed: {error}") @@ -243,4 +243,4 @@ public static void SortedUuidsInsert(ProcedureContext ctx) return 0; }); } -} \ No newline at end of file +} diff --git a/modules/sdk-test-procedure-ts/src/index.ts b/modules/sdk-test-procedure-ts/src/index.ts index f89aa76665a..508599489c2 100644 --- a/modules/sdk-test-procedure-ts/src/index.ts +++ b/modules/sdk-test-procedure-ts/src/index.ts @@ -90,13 +90,17 @@ export const will_panic = spacetimedb.procedure(t.unit(), _ctx => { throw new Error('This procedure is expected to panic'); }); -export const read_my_schema = spacetimedb.procedure(t.string(), ctx => { - const module_identity = ctx.databaseIdentity; - const response = ctx.http.fetch( - `http://localhost:3000/v1/database/${module_identity}/schema?version=9` - ); - return response.text(); -}); +export const read_my_schema = spacetimedb.procedure( + { server_url: t.string() }, + t.string(), + (ctx, { server_url }) => { + const module_identity = ctx.databaseIdentity; + const response = ctx.http.fetch( + `${server_url}/v1/database/${module_identity}/schema?version=9` + ); + return response.text(); + } +); export const invalid_request = spacetimedb.procedure(t.string(), ctx => { try {