diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bb00eb6f12..7f320470842 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/Cargo.lock b/Cargo.lock index 9cc97fceadc..a51c74638ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8136,6 +8136,16 @@ dependencies = [ "wasmtime-internal-fiber", ] +[[package]] +name = "spacetimedb-csharp-tests" +version = "2.2.0" +dependencies = [ + "anyhow", + "clap 4.5.50", + "quick-xml 0.31.0", + "spacetimedb-language-test-support", +] + [[package]] name = "spacetimedb-data-structures" version = "2.2.0" @@ -8296,6 +8306,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "spacetimedb-language-test-support" +version = "2.2.0" +dependencies = [ + "anyhow", + "spacetimedb-guard", +] + [[package]] name = "spacetimedb-lib" version = "1.9.0" @@ -8784,6 +8802,16 @@ dependencies = [ "wasmbin", ] +[[package]] +name = "spacetimedb-typescript-tests" +version = "2.2.0" +dependencies = [ + "anyhow", + "clap 4.5.50", + "quick-xml 0.31.0", + "spacetimedb-language-test-support", +] + [[package]] name = "spacetimedb-update" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 120ec24c2cf..511bf69e9c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ members = [ "crates/execution", "crates/expr", "crates/guard", + "crates/language-test-support", + "crates/typescript-tests", + "crates/csharp-tests", "crates/fs-utils", "crates/lib", "crates/metrics", @@ -130,6 +133,7 @@ spacetimedb-durability = { path = "crates/durability", version = "=2.2.0" } spacetimedb-execution = { path = "crates/execution", version = "=2.2.0" } spacetimedb-expr = { path = "crates/expr", version = "=2.2.0" } spacetimedb-guard = { path = "crates/guard", version = "=2.2.0" } +spacetimedb-language-test-support = { path = "crates/language-test-support", version = "=2.2.0" } spacetimedb-lib = { path = "crates/lib", default-features = false, version = "=2.2.0" } spacetimedb-memory-usage = { path = "crates/memory-usage", version = "=2.2.0", default-features = false } spacetimedb-metrics = { path = "crates/metrics", version = "=2.2.0" } diff --git a/crates/csharp-tests/Cargo.toml b/crates/csharp-tests/Cargo.toml new file mode 100644 index 00000000000..706271f5d29 --- /dev/null +++ b/crates/csharp-tests/Cargo.toml @@ -0,0 +1,20 @@ +[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 +clap.workspace = true +quick-xml.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..8d415d14f0e --- /dev/null +++ b/crates/csharp-tests/tests/csharp.rs @@ -0,0 +1,271 @@ +#![allow(clippy::disallowed_macros)] + +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, SpacetimeDbGuard, TestCaseResult}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; + +#[derive(Clone, Debug, Default, Parser)] +struct Args { + #[arg(long)] + filter: Option, + + #[arg(long, alias = "list-tests")] + list: bool, + + #[arg(last = true)] + passthrough: Vec, +} + +fn main() { + if let Err(err) = run() { + eprintln!("{err:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + + let workspace = spacetimedb_language_test_support::workspace_root(); + let out_dir = target_dir().join("csharp-tests"); + 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"))?; + return Ok(()); + } + + run_dotnet_test( + "csharp sdk", + &workspace.join("sdks/csharp"), + &out_dir, + "sdk.trx", + args.filter.as_deref(), + &args.passthrough, + )?; + run_regression_tests(&workspace)?; + + Ok(()) +} + +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) + .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, + out_dir: &Path, + report_name: &str, + filter: Option<&str>, + passthrough: &[String], +) -> Result<()> { + let report = out_dir.join(report_name); + + 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}"), + "--no-restore".to_string(), + ]; + if let Some(filter) = filter { + test_args.push("--filter".to_string()); + test_args.push(filter.to_string()); + } + test_args.extend(passthrough.iter().cloned()); + + let command_line = shell_line("dotnet", &test_args); + let status = Command::new("dotnet") + .args(&test_args) + .current_dir(cwd) + .status() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + 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)?; + 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<()> { + let status = Command::new("dotnet") + .args(["pack", "crates/bindings-csharp/BSATN.Runtime"]) + .current_dir(workspace) + .status() + .with_context(|| { + format!( + "failed to spawn `dotnet pack crates/bindings-csharp/BSATN.Runtime` in {}", + workspace.display() + ) + })?; + ensure_success(workspace, "dotnet pack crates/bindings-csharp/BSATN.Runtime", status)?; + + let status = Command::new("dotnet") + .args(["pack", "crates/bindings-csharp/Runtime"]) + .current_dir(workspace) + .status() + .with_context(|| { + format!( + "failed to spawn `dotnet pack crates/bindings-csharp/Runtime` in {}", + workspace.display() + ) + })?; + 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 + // 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 status = Command::new("bash") + .args(["./tools~/write-nuget-config.sh", "../.."]) + .current_dir(&cwd) + .status() + .with_context(|| { + format!( + "failed to spawn `bash ./tools~/write-nuget-config.sh ../..` in {}", + cwd.display() + ) + })?; + ensure_success(&cwd, "bash ./tools~/write-nuget-config.sh ../..", status)?; + + let status = Command::new("dotnet") + .args(["restore", "--configfile", "NuGet.Config", "SpacetimeDB.ClientSDK.sln"]) + .current_dir(&cwd) + .status() + .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", + status, + )?; + Ok(()) +} + +fn run_regression_tests(workspace: &Path) -> Result<()> { + let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); + let cwd = workspace.join("sdks/csharp"); + let status = Command::new("bash") + .args(["tools~/run-regression-tests.sh"]) + .current_dir(&cwd) + .env("SPACETIMEDB_SERVER_URL", &guard.host_url) + .status() + .with_context(|| { + format!( + "failed to spawn `bash tools~/run-regression-tests.sh` in {}", + cwd.display() + ) + })?; + ensure_success(&cwd, "bash tools~/run-regression-tests.sh", status)?; + Ok(()) +} + +fn ensure_success(cwd: &Path, command_line: &str, status: ExitStatus) -> Result<()> { + if status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}", + cwd.display(), + command_line, + status + ); +} + +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/language-test-support/Cargo.toml b/crates/language-test-support/Cargo.toml new file mode 100644 index 00000000000..eefe24857ac --- /dev/null +++ b/crates/language-test-support/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "spacetimedb-language-test-support" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow.workspace = true +spacetimedb-guard.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..7f2d770fa46 --- /dev/null +++ b/crates/language-test-support/src/lib.rs @@ -0,0 +1,66 @@ +#![allow(clippy::disallowed_macros)] + +use anyhow::{bail, Result}; +use std::env; +use std::path::{Path, PathBuf}; + +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, +} + +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 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..3e97833dc7b --- /dev/null +++ b/crates/typescript-tests/Cargo.toml @@ -0,0 +1,20 @@ +[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 +clap.workspace = true +quick-xml.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..64671728a12 --- /dev/null +++ b/crates/typescript-tests/tests/typescript.rs @@ -0,0 +1,181 @@ +#![allow(clippy::disallowed_macros)] + +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::fs; +use std::path::Path; +use std::process::{Command, ExitStatus}; + +#[derive(Clone, Debug, Default, Parser)] +struct Args { + #[arg(long)] + filter: Option, + + #[arg(long, alias = "list-tests")] + list: bool, + + #[arg(last = true)] + passthrough: Vec, +} + +fn main() { + if let Err(err) = run() { + eprintln!("{err:#}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + + let workspace = spacetimedb_language_test_support::workspace_root(); + let cwd = workspace.join("crates/bindings-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"); + + if args.list { + return list_tests(&cwd, args.filter); + } + + run_tests(&cwd, &report, args.filter, args.passthrough) +} + +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 command_line = shell_line("pnpm", &cmd); + let status = Command::new("pnpm") + .args(&cmd) + .current_dir(cwd) + .status() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, status)?; + Ok(()) +} + +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") + .args(&build_args) + .current_dir(cwd) + .status() + .with_context(|| format!("failed to spawn `{command_line}` in {}", cwd.display()))?; + ensure_success(cwd, &command_line, status)?; + + let mut test_args = vec![ + "test".to_string(), + "--".to_string(), + "--reporter=default".to_string(), + "--reporter=junit".to_string(), + format!("--outputFile={}", report.display()), + ]; + if let Some(filter) = filter { + test_args.push("-t".to_string()); + test_args.push(filter); + } + test_args.extend(passthrough); + let command_line = shell_line("pnpm", &test_args); + let status = Command::new("pnpm") + .args(&test_args) + .current_dir(cwd) + .status() + .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)?; + + Ok(()) +} + +fn ensure_success(cwd: &Path, command_line: &str, status: ExitStatus) -> Result<()> { + if status.success() { + return Ok(()); + } + + bail!( + "command failed in {}:\n {}\nstatus: {}", + cwd.display(), + command_line, + status + ); +} + +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-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 { 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 { 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..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.0.5 (commit ca7484e072f9514fb2f890f26600a5d096f59431). +// 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}; 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; diff --git a/tools/ci/README.md b/tools/ci/README.md index 3530ee24b5e..fca16e0b52f 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 @@ -217,6 +217,17 @@ Usage: typescript-test - `--help`: Print help +### `csharp-tests` + +**Usage:** +```bash +Usage: csharp-tests +``` + +**Options:** + +- `--help`: Print help + ### `version-upgrade-check` **Usage:** diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 146651c1053..148cac0e31e 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, /// Verifies that the repository version upgrade tool still works. VersionUpgradeCheck, /// Builds the docs site. @@ -417,8 +419,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", @@ -439,6 +448,54 @@ fn run_typescript_tests() -> Result<()> { Ok(()) } +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()?; + 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").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()?; @@ -481,6 +538,10 @@ fn main() -> Result<()> { "spacetimedb-smoketests", "--exclude", "spacetimedb-sdk", + "--exclude", + "spacetimedb-typescript-tests", + "--exclude", + "spacetimedb-csharp-tests", "--", "--test-threads=2", "--skip", @@ -532,19 +593,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()?; - cmd!("dotnet", "test", "-warnaserror") - .dir("crates/bindings-csharp") - .run()?; } Some(CiCmd::Lint) => { @@ -728,6 +776,10 @@ fn main() -> Result<()> { run_typescript_tests()?; } + Some(CiCmd::CsharpTests) => { + run_csharp_tests()?; + } + Some(CiCmd::VersionUpgradeCheck) => { run_version_upgrade_check()?; } 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, }