diff --git a/Cargo.lock b/Cargo.lock index efc58dc007b..9eafc9ba891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8748,6 +8748,7 @@ dependencies = [ "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-data-structures", + "spacetimedb-guard", "spacetimedb-lib", "spacetimedb-paths", "spacetimedb-schema", diff --git a/crates/bindings-typescript/case-conversion-test-client/src/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/index.ts index 8f7567e312f..07cf69d44ca 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/index.ts @@ -19,7 +19,10 @@ import { DbConnection, tables } from './module_bindings/index.js'; -const LOCALHOST = 'http://localhost:3000'; +const SERVER_URL = process.env.SPACETIME_SDK_TEST_SERVER_URL; +if (!SERVER_URL) { + throw new Error('Missing SPACETIME_SDK_TEST_SERVER_URL'); +} function dbNameOrPanic(): string { const name = process.env.SPACETIME_SDK_TEST_DB_NAME; @@ -53,7 +56,7 @@ function connectThen(callback: (db: DbConnection) => void): Promise { return new Promise((resolve, reject) => { const conn = DbConnection.builder() .withDatabaseName(name) - .withUri(LOCALHOST) + .withUri(SERVER_URL) .onConnect((ctx, _identity, _token) => { try { callback(ctx); diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index bf8c305526d..84295a77f46 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -16,6 +16,7 @@ spacetimedb-core.workspace = true spacetimedb-standalone.workspace = true spacetimedb-client-api.workspace = true spacetimedb-client-api-messages.workspace = true +spacetimedb-guard.workspace = true spacetimedb-paths.workspace = true spacetimedb-schema.workspace = true diff --git a/crates/testing/src/sdk.rs b/crates/testing/src/sdk.rs index e995aa28019..eb45353aa2b 100644 --- a/crates/testing/src/sdk.rs +++ b/crates/testing/src/sdk.rs @@ -2,64 +2,26 @@ use duct::cmd; use rand::seq::IteratorRandom; use spacetimedb::messages::control_db::HostType; use spacetimedb_data_structures::map::HashMap; +use spacetimedb_guard::SpacetimeDbGuard; use spacetimedb_paths::{RootDir, SpacetimePaths}; use std::fs::create_dir_all; -use std::sync::{Mutex, OnceLock}; -use std::thread::JoinHandle; +use std::sync::Mutex; use crate::invoke_cli; -use crate::modules::{start_runtime, CompilationMode, CompiledModule}; +use crate::modules::{CompilationMode, CompiledModule}; use tempfile::TempDir; -/// Ensure that the server thread we're testing against is still running, starting -/// it if it hasn't been started yet. -pub fn ensure_standalone_process() -> &'static SpacetimePaths { - static PATHS: OnceLock = OnceLock::new(); - static JOIN_HANDLE: OnceLock>>>> = OnceLock::new(); - - let paths = PATHS.get_or_init(|| { - let dir = TempDir::with_prefix("stdb-sdk-test") - .expect("Failed to create tempdir") - // TODO: This leaks the tempdir. - // We need the tempdir to live for the duration of the process, - // and all the options for post-`main` cleanup seem sketchy. - .keep(); - SpacetimePaths::from_root_dir(&RootDir(dir)) - }); - - let join_handle = JOIN_HANDLE.get_or_init(|| { - Mutex::new(Some(std::thread::spawn(move || { - start_runtime().block_on(spacetimedb_standalone::start_server( - &paths.data_dir, - Some(&paths.cli_config_dir.0), - )) - }))) - }); - - let mut join_handle = join_handle.lock().unwrap_or_else(|e| e.into_inner()); - - if join_handle - .as_ref() - .expect("Standalone process already finished") - .is_finished() - { - match join_handle.take().unwrap().join() { - Ok(Ok(())) => {} - Ok(Err(e)) => panic!("standalone process failed: {e:?}"), - Err(e) => { - let msg = if let Some(s) = e.downcast_ref::() { - s - } else if let Some(s) = e.downcast_ref::<&str>() { - s - } else { - "dyn Any" - }; - panic!("standalone process failed by panic: {msg}") - } - } - } +struct SdkTestPaths { + paths: SpacetimePaths, + _root: TempDir, +} - paths +impl SdkTestPaths { + fn new() -> Self { + let root = TempDir::with_prefix("stdb-sdk-test").expect("Failed to create tempdir"); + let paths = SpacetimePaths::from_root_dir(&RootDir(root.path().to_path_buf())); + Self { paths, _root: root } + } } pub struct Test { @@ -105,11 +67,13 @@ pub struct Test { /// Will run with access to the env vars: /// - `SPACETIME_SDK_TEST_CLIENT_PROJECT` bound to the `client_project` path. /// - `SPACETIME_SDK_TEST_DB_NAME` bound to the database identity or name. + /// - `SPACETIME_SDK_TEST_SERVER_URL` bound to the server URL for this test. run_command: String, } pub const TEST_MODULE_PROJECT_ENV_VAR: &str = "SPACETIME_SDK_TEST_MODULE_PROJECT"; pub const TEST_DB_NAME_ENV_VAR: &str = "SPACETIME_SDK_TEST_DB_NAME"; +pub const TEST_SERVER_URL_ENV_VAR: &str = "SPACETIME_SDK_TEST_SERVER_URL"; pub const TEST_CLIENT_PROJECT_ENV_VAR: &str = "SPACETIME_SDK_TEST_CLIENT_PROJECT"; fn language_is_unreal(language: &str) -> bool { @@ -121,7 +85,8 @@ impl Test { TestBuilder::default() } pub fn run(self) { - let paths = ensure_standalone_process(); + let sdk_paths = SdkTestPaths::new(); + let paths = &sdk_paths.paths; let (file, host_type) = compile_module(&self.module_name); @@ -137,9 +102,11 @@ impl Test { compile_client(&self.compile_command, &self.client_project); - let db_name = publish_module(paths, &file, host_type); + let guard = SpacetimeDbGuard::spawn_in_temp_data_dir(); + let server_url = guard.host_url.as_str(); + let db_name = publish_module(paths, server_url, &file, host_type); - run_client(&self.run_command, &self.client_project, &db_name); + run_client(&self.run_command, &self.client_project, server_url, &db_name); } } @@ -213,7 +180,7 @@ fn compile_module(module: &str) -> (String, HostType) { // Note: this function does not memoize because we want each test to publish the same // module as a separate clean database instance for isolation purposes. -fn publish_module(paths: &SpacetimePaths, wasm_file: &str, host_type: HostType) -> String { +fn publish_module(paths: &SpacetimePaths, server_url: &str, wasm_file: &str, host_type: HostType) -> String { let name = random_module_name(); invoke_cli( paths, @@ -221,7 +188,7 @@ fn publish_module(paths: &SpacetimePaths, wasm_file: &str, host_type: HostType) "publish", "--anonymous", "--server", - "local", + server_url, match host_type { HostType::Wasm => "--bin-path", HostType::Js => "--js-path", @@ -268,10 +235,7 @@ fn publish_module(paths: &SpacetimePaths, wasm_file: &str, host_type: HostType) /// If you need bindings for multiple different modules, put them in different subdirs. /// - If multiple distinct test harness processes run concurrently, /// they will encounter the race condition described above, -/// because the `BINDINGS_GENERATED` lock is not shared between harness processes. -/// Running multiple test harness processes concurrently will break anyways -/// because each will try to run `spacetime start` as a subprocess and will therefore -/// contend over port 3000. +/// because the binding-generation lock is not shared between harness processes. /// Prefer constructing multiple `Test`s and `Test::run`ing them /// from within the same harness process. // @@ -384,12 +348,13 @@ fn compile_client(compile_command: &str, client_project: &str) { }) } -fn run_client(run_command: &str, client_project: &str, db_name: &str) { +fn run_client(run_command: &str, client_project: &str, server_url: &str, db_name: &str) { let (exe, args) = split_command_string(run_command); let output = cmd(exe, args) .dir(client_project) .env(TEST_CLIENT_PROJECT_ENV_VAR, client_project) + .env(TEST_SERVER_URL_ENV_VAR, server_url) .env(TEST_DB_NAME_ENV_VAR, db_name) .env( "RUST_LOG", diff --git a/modules/sdk-test-procedure-cpp/src/lib.cpp b/modules/sdk-test-procedure-cpp/src/lib.cpp index 31e3669703a..da1278ccdca 100644 --- a/modules/sdk-test-procedure-cpp/src/lib.cpp +++ b/modules/sdk-test-procedure-cpp/src/lib.cpp @@ -140,15 +140,18 @@ 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(); + while (!server_url.empty() && server_url.back() == '/') { + server_url.pop_back(); + } 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..2e405c3a9bc 100644 --- a/modules/sdk-test-procedure-cs/Lib.cs +++ b/modules/sdk-test-procedure-cs/Lib.cs @@ -66,10 +66,11 @@ 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"); + serverUrl = serverUrl.TrimEnd('/'); + 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 +244,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..1885eafd156 100644 --- a/modules/sdk-test-procedure-ts/src/index.ts +++ b/modules/sdk-test-procedure-ts/src/index.ts @@ -90,13 +90,18 @@ 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 base_url = server_url.replace(/\/+$/, ''); + const response = ctx.http.fetch( + `${base_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..2c51e7ee26f 100644 --- a/modules/sdk-test-procedure/src/lib.rs +++ b/modules/sdk-test-procedure/src/lib.rs @@ -41,11 +41,13 @@ 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" - )) { + let server_url = server_url.trim_end_matches('/'); + 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/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/rust/tests/case-conversion-client/src/main.rs b/sdks/rust/tests/case-conversion-client/src/main.rs index 606b22dfbae..3bcc10dadee 100644 --- a/sdks/rust/tests/case-conversion-client/src/main.rs +++ b/sdks/rust/tests/case-conversion-client/src/main.rs @@ -8,9 +8,7 @@ use module_bindings::*; use spacetimedb_sdk::error::InternalError; use spacetimedb_sdk::{DbContext, Table, TableWithPrimaryKey}; use std::sync::Arc; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; fn db_name_or_panic() -> String { std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env") @@ -83,7 +81,7 @@ fn connect_then( let name = db_name_or_panic(); let conn = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(move |ctx, _, _| { callback(ctx); connected_result(Ok(())); diff --git a/sdks/rust/tests/connect_disconnect_client/src/lib.rs b/sdks/rust/tests/connect_disconnect_client/src/lib.rs index da4421aa5e0..b86eda1125f 100644 --- a/sdks/rust/tests/connect_disconnect_client/src/lib.rs +++ b/sdks/rust/tests/connect_disconnect_client/src/lib.rs @@ -8,9 +8,10 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(_test_name: String, db_name: String) { +pub async fn run(_test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); // The shared wasm test harness always passes `(test_name, db_name)`, even for // fixed-flow clients like this one that ignore the selector. + test_counter::set_server_url(server_url); test_handlers::dispatch(&db_name).await; } diff --git a/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs b/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs index 56753d78039..a7795e8530b 100644 --- a/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs +++ b/sdks/rust/tests/connect_disconnect_client/src/test_handlers.rs @@ -2,9 +2,7 @@ use crate::module_bindings::*; use spacetimedb_sdk::{DbConnectionBuilder, DbContext, Table}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; pub async fn dispatch(db_name: &str) { let disconnect_test_counter = TestCounter::new(); @@ -16,7 +14,7 @@ pub async fn dispatch(db_name: &str) { let connection = DbConnection::builder() .with_database_name(db_name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) .on_connect(move |ctx, _, _| { connected_result(Ok(())); @@ -80,7 +78,7 @@ pub async fn dispatch(db_name: &str) { reconnected_result(Ok(())); }) .with_database_name(db_name) - .with_uri(LOCALHOST); + .with_uri(server_url()); let new_connection = build_connection(new_connection).await; new_connection diff --git a/sdks/rust/tests/event-table-client/src/lib.rs b/sdks/rust/tests/event-table-client/src/lib.rs index 6a3a638c5c2..04d313e5699 100644 --- a/sdks/rust/tests/event-table-client/src/lib.rs +++ b/sdks/rust/tests/event-table-client/src/lib.rs @@ -8,9 +8,10 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); - // The shared wasm test harness passes both the selected test name and the - // published database name. wasm clients cannot rely on the native env-var path. + // The shared wasm test harness passes test settings explicitly + // WASM clients cannot rely on the native env-var path. + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/event-table-client/src/main.rs b/sdks/rust/tests/event-table-client/src/main.rs index a2a79eb19fe..d5e92029c1c 100644 --- a/sdks/rust/tests/event-table-client/src/main.rs +++ b/sdks/rust/tests/event-table-client/src/main.rs @@ -17,7 +17,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - // Keep the CLI entrypoint thin so both native and wasm execute the same handlers. tokio::runtime::Runtime::new() .unwrap() diff --git a/sdks/rust/tests/event-table-client/src/test_handlers.rs b/sdks/rust/tests/event-table-client/src/test_handlers.rs index f77c78e3896..3e361125c66 100644 --- a/sdks/rust/tests/event-table-client/src/test_handlers.rs +++ b/sdks/rust/tests/event-table-client/src/test_handlers.rs @@ -2,9 +2,7 @@ use crate::module_bindings::*; use spacetimedb_sdk::{DbConnectionBuilder, DbContext, Event, EventTable}; use std::sync::atomic::{AtomicU32, Ordering}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; macro_rules! assert_eq_or_bail { ($expected:expr, $found:expr) => {{ @@ -57,7 +55,7 @@ async fn connect_then( let name = db_name.to_owned(); let conn = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); diff --git a/sdks/rust/tests/procedural-view-pk-client/src/lib.rs b/sdks/rust/tests/procedural-view-pk-client/src/lib.rs index 0ffa7e5dacd..c2f33c2db84 100644 --- a/sdks/rust/tests/procedural-view-pk-client/src/lib.rs +++ b/sdks/rust/tests/procedural-view-pk-client/src/lib.rs @@ -8,7 +8,8 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/procedural-view-pk-client/src/main.rs b/sdks/rust/tests/procedural-view-pk-client/src/main.rs index 5a8fdf8970e..4681a87c617 100644 --- a/sdks/rust/tests/procedural-view-pk-client/src/main.rs +++ b/sdks/rust/tests/procedural-view-pk-client/src/main.rs @@ -16,7 +16,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - tokio::runtime::Runtime::new() .unwrap() .block_on(test_handlers::dispatch(&test, &db_name)); diff --git a/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs index 4404d3715b1..319faf66b8b 100644 --- a/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedural-view-pk-client/src/test_handlers.rs @@ -1,8 +1,6 @@ use crate::module_bindings::*; use spacetimedb_sdk::{error::InternalError, DbConnectionBuilder, DbContext, Table, TableWithPrimaryKey}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; type ResultRecorder = Box)>; @@ -52,7 +50,7 @@ async fn connect_then_named( let name = db_name.to_owned(); let conn = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); diff --git a/sdks/rust/tests/procedure-client/src/lib.rs b/sdks/rust/tests/procedure-client/src/lib.rs index 6a3a638c5c2..04d313e5699 100644 --- a/sdks/rust/tests/procedure-client/src/lib.rs +++ b/sdks/rust/tests/procedure-client/src/lib.rs @@ -8,9 +8,10 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); - // The shared wasm test harness passes both the selected test name and the - // published database name. wasm clients cannot rely on the native env-var path. + // The shared wasm test harness passes test settings explicitly + // WASM clients cannot rely on the native env-var path. + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/procedure-client/src/main.rs b/sdks/rust/tests/procedure-client/src/main.rs index 6739650776e..c6de4033072 100644 --- a/sdks/rust/tests/procedure-client/src/main.rs +++ b/sdks/rust/tests/procedure-client/src/main.rs @@ -22,7 +22,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - // Keep the CLI entrypoint thin so both native and wasm execute the same handlers. tokio::runtime::Runtime::new() .unwrap() 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..fdfc417cd9b 100644 --- a/sdks/rust/tests/procedure-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedure-client/src/test_handlers.rs @@ -3,9 +3,7 @@ use anyhow::Context; use core::time::Duration; use spacetimedb_lib::db::raw_def::v9::{RawMiscModuleExportV9, RawModuleDefV9}; use spacetimedb_sdk::{DbConnectionBuilder, DbContext, Table}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; pub async fn dispatch(test: &str, db_name: &str) { match test { @@ -69,7 +67,7 @@ async fn connect_with_then( let name = db_name.to_owned(); let builder = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); @@ -257,26 +255,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(server_url().to_string(), 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/sdks/rust/tests/procedure-concurrency-client/src/lib.rs b/sdks/rust/tests/procedure-concurrency-client/src/lib.rs index 0ffa7e5dacd..c2f33c2db84 100644 --- a/sdks/rust/tests/procedure-concurrency-client/src/lib.rs +++ b/sdks/rust/tests/procedure-concurrency-client/src/lib.rs @@ -8,7 +8,8 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/procedure-concurrency-client/src/main.rs b/sdks/rust/tests/procedure-concurrency-client/src/main.rs index 27b2f47c453..1ec4d15816c 100644 --- a/sdks/rust/tests/procedure-concurrency-client/src/main.rs +++ b/sdks/rust/tests/procedure-concurrency-client/src/main.rs @@ -16,7 +16,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - tokio::runtime::Runtime::new() .unwrap() .block_on(test_handlers::dispatch(&test, &db_name)); diff --git a/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs b/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs index fa67bc11692..a7ab7df2e86 100644 --- a/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs +++ b/sdks/rust/tests/procedure-concurrency-client/src/test_handlers.rs @@ -2,9 +2,7 @@ use crate::module_bindings::*; use anyhow::Context; use spacetimedb_sdk::{DbConnectionBuilder, DbContext, Table}; use std::sync::{Arc, Mutex}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; pub async fn dispatch(test: &str, db_name: &str) { match test { @@ -62,7 +60,7 @@ async fn connect_with_then( let name = db_name.to_owned(); let builder = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); diff --git a/sdks/rust/tests/test-client/src/lib.rs b/sdks/rust/tests/test-client/src/lib.rs index 8ef2391750b..9fdfec32789 100644 --- a/sdks/rust/tests/test-client/src/lib.rs +++ b/sdks/rust/tests/test-client/src/lib.rs @@ -13,7 +13,8 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/test-client/src/main.rs b/sdks/rust/tests/test-client/src/main.rs index 1e305289493..d83b104f28e 100644 --- a/sdks/rust/tests/test-client/src/main.rs +++ b/sdks/rust/tests/test-client/src/main.rs @@ -22,7 +22,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - // Keep the CLI entrypoint thin so both native and wasm execute the same handlers. tokio::runtime::Runtime::new() .unwrap() diff --git a/sdks/rust/tests/test-client/src/test_handlers.rs b/sdks/rust/tests/test-client/src/test_handlers.rs index d83ee46cf4d..98b21e0c2cd 100644 --- a/sdks/rust/tests/test-client/src/test_handlers.rs +++ b/sdks/rust/tests/test-client/src/test_handlers.rs @@ -13,7 +13,7 @@ use spacetimedb_sdk::{ i256, u256, Compression, ConnectionId, DbConnectionBuilder, DbContext, Event, Identity, ReducerEvent, Status, SubscriptionHandle, Table, TimeDuration, Timestamp, Uuid, }; -use test_counter::TestCounter; +use test_counter::{server_url, TestCounter}; use crate::simple_test_table::{insert_one, on_insert_one, SimpleTestTable}; @@ -21,8 +21,6 @@ use crate::pk_test_table::{insert_update_delete_one, PkTestTable}; use crate::unique_test_table::{insert_then_delete_one, UniqueTestTable}; -const LOCALHOST: &str = "http://localhost:3000"; - /// `Timestamp::now()` is stubbed on `wasm32-unknown-unknown`, so client-side tests /// that need a timestamp value must use a deterministic literal instead of wall-clock time. fn fixed_test_timestamp() -> Timestamp { @@ -89,6 +87,7 @@ pub async fn dispatch(test: &str, db_name: &str) { // "resubscribe" => exec_resubscribe(), // + "reauth" => exec_reauth(db_name).await, "reauth-part-1" => exec_reauth_part_1(db_name).await, "reauth-part-2" => exec_reauth_part_2(db_name).await, @@ -372,7 +371,7 @@ async fn connect_with_then( let name = db_name.to_owned(); let builder = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); @@ -1724,8 +1723,13 @@ async fn exec_insert_primitives_as_strings(db_name: &str) { // } #[cfg(not(target_arch = "wasm32"))] -fn creds_store() -> credentials::File { - credentials::File::new("rust-sdk-test") +fn creds_store(db_name: &str) -> credentials::File { + credentials::File::new(format!("rust-sdk-test-{db_name}")) +} + +async fn exec_reauth(db_name: &str) { + exec_reauth_part_1(db_name).await; + exec_reauth_part_2(db_name).await; } /// Part of the `reauth` test, this connects to Spacetime to get new credentials, @@ -1737,14 +1741,15 @@ async fn exec_reauth_part_1(db_name: &str) { let name = db_name.to_owned(); let save_result = test_counter.add_test("save-credentials"); + let creds = creds_store(db_name); DbConnection::builder() - .on_connect(|_, _identity, token| { - save_result(creds_store().save(token).map_err(Into::into)); + .on_connect(move |_, _identity, token| { + save_result(creds.save(token).map_err(Into::into)); }) .on_connect_error(|_ctx, error| panic!("Connect failed: {error:?}")) .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .build() .unwrap() .run_threaded(); @@ -1764,7 +1769,7 @@ async fn exec_reauth_part_2(db_name: &str) { let creds_match_result = test_counter.add_test("creds-match"); - let token = creds_store().load().unwrap().unwrap(); + let token = creds_store(db_name).load().unwrap().unwrap(); DbConnection::builder() .on_connect({ @@ -1780,7 +1785,7 @@ async fn exec_reauth_part_2(db_name: &str) { .on_connect_error(|_ctx, error| panic!("Connect failed: {error:?}")) .with_database_name(name) .with_token(Some(token)) - .with_uri(LOCALHOST) + .with_uri(server_url()) .build() .unwrap() .run_threaded(); @@ -1811,7 +1816,7 @@ async fn exec_reconnect_different_connection_id(db_name: &str) { let initial_connection = build_and_run( DbConnection::builder() .with_database_name(db_name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) .on_connect(move |_, _, _| { initial_connect_result(Ok(())); @@ -1838,7 +1843,7 @@ async fn exec_reconnect_different_connection_id(db_name: &str) { let _re_connection = build_and_run( DbConnection::builder() .with_database_name(db_name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect_error(|_ctx, error| panic!("on_connect_error: {error:?}")) .on_connect(move |ctx, _, _| { reconnect_result(Ok(())); diff --git a/sdks/rust/tests/test-counter/src/lib.rs b/sdks/rust/tests/test-counter/src/lib.rs index 329d29f7025..aa774ae09fa 100644 --- a/sdks/rust/tests/test-counter/src/lib.rs +++ b/sdks/rust/tests/test-counter/src/lib.rs @@ -1,11 +1,37 @@ #![allow(clippy::disallowed_macros)] use spacetimedb_data_structures::map::{HashMap, HashSet}; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::{Arc, Condvar, Mutex, OnceLock}; #[cfg(not(target_arch = "wasm32"))] use std::time::Duration; const TEST_TIMEOUT_SECS: u64 = 5 * 60; +pub const TEST_SERVER_URL_ENV_VAR: &str = "SPACETIME_SDK_TEST_SERVER_URL"; + +static SERVER_URL: OnceLock = OnceLock::new(); + +pub fn set_server_url(url: String) { + if SERVER_URL.set(url).is_err() { + panic!("{TEST_SERVER_URL_ENV_VAR} was set more than once"); + } +} + +pub fn server_url() -> &'static str { + SERVER_URL.get_or_init(server_url_from_env).as_str() +} + +fn server_url_from_env() -> String { + #[cfg(not(target_arch = "wasm32"))] + { + std::env::var(TEST_SERVER_URL_ENV_VAR) + .unwrap_or_else(|_| panic!("{TEST_SERVER_URL_ENV_VAR} must be set by the SDK test harness")) + } + + #[cfg(target_arch = "wasm32")] + { + panic!("{TEST_SERVER_URL_ENV_VAR} must be passed to the wasm SDK test harness") + } +} #[derive(Default)] struct TestCounterInner { diff --git a/sdks/rust/tests/test.rs b/sdks/rust/tests/test.rs index 0714033956f..85cb15a970a 100644 --- a/sdks/rust/tests/test.rs +++ b/sdks/rust/tests/test.rs @@ -62,7 +62,9 @@ fn platform_test_builder(client_project: &str, run_selector: Option<&str>) -> Te if (!run) throw new Error(\"No exported run/main/start function from wasm module\"); \ const dbName = process.env.SPACETIME_SDK_TEST_DB_NAME; \ if (!dbName) throw new Error(\"Missing SPACETIME_SDK_TEST_DB_NAME\"); \ - await run({run_selector:?}, dbName); \ + const serverUrl = process.env.SPACETIME_SDK_TEST_SERVER_URL; \ + if (!serverUrl) throw new Error(\"Missing SPACETIME_SDK_TEST_SERVER_URL\"); \ + await run({run_selector:?}, dbName, serverUrl); \ // These wasm clients run under Node rather than a browser. Some tests intentionally leave // websocket/event-loop work alive once their assertions are complete, so exit here to keep // non-lifecycle tests from hanging on leftover handles after `run()` has finished. @@ -275,8 +277,7 @@ macro_rules! declare_tests_with_suffix { #[test] fn reauth() { - make_test("reauth-part-1").run(); - make_test("reauth-part-2").run(); + make_test("reauth").run(); } #[test] diff --git a/sdks/rust/tests/view-client/src/lib.rs b/sdks/rust/tests/view-client/src/lib.rs index 6a3a638c5c2..04d313e5699 100644 --- a/sdks/rust/tests/view-client/src/lib.rs +++ b/sdks/rust/tests/view-client/src/lib.rs @@ -8,9 +8,10 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); - // The shared wasm test harness passes both the selected test name and the - // published database name. wasm clients cannot rely on the native env-var path. + // The shared wasm test harness passes test settings explicitly + // WASM clients cannot rely on the native env-var path. + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/view-client/src/main.rs b/sdks/rust/tests/view-client/src/main.rs index 0bc8a792198..bca3fa2307e 100644 --- a/sdks/rust/tests/view-client/src/main.rs +++ b/sdks/rust/tests/view-client/src/main.rs @@ -22,7 +22,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - // Keep the CLI entrypoint thin so both native and wasm execute the same handlers. tokio::runtime::Runtime::new() .unwrap() diff --git a/sdks/rust/tests/view-client/src/test_handlers.rs b/sdks/rust/tests/view-client/src/test_handlers.rs index 21b9dc43ae7..c8b0743166b 100644 --- a/sdks/rust/tests/view-client/src/test_handlers.rs +++ b/sdks/rust/tests/view-client/src/test_handlers.rs @@ -1,9 +1,7 @@ use crate::module_bindings::*; use spacetimedb_lib::Identity; use spacetimedb_sdk::{error::InternalError, DbConnectionBuilder, DbContext, Table}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; pub async fn dispatch(test: &str, db_name: &str) { match test { @@ -43,7 +41,7 @@ async fn build_connection( let name = db_name.to_owned(); let builder = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); }) diff --git a/sdks/rust/tests/view-pk-client/src/lib.rs b/sdks/rust/tests/view-pk-client/src/lib.rs index 6a3a638c5c2..04d313e5699 100644 --- a/sdks/rust/tests/view-pk-client/src/lib.rs +++ b/sdks/rust/tests/view-pk-client/src/lib.rs @@ -8,9 +8,10 @@ use wasm_bindgen::prelude::wasm_bindgen; #[cfg(all(target_arch = "wasm32", feature = "browser"))] #[wasm_bindgen] -pub async fn run(test_name: String, db_name: String) { +pub async fn run(test_name: String, db_name: String, server_url: String) { console_error_panic_hook::set_once(); - // The shared wasm test harness passes both the selected test name and the - // published database name. wasm clients cannot rely on the native env-var path. + // The shared wasm test harness passes test settings explicitly + // WASM clients cannot rely on the native env-var path. + test_counter::set_server_url(server_url); test_handlers::dispatch(&test_name, &db_name).await; } diff --git a/sdks/rust/tests/view-pk-client/src/main.rs b/sdks/rust/tests/view-pk-client/src/main.rs index 3e1c03945ef..b8bd4bad76e 100644 --- a/sdks/rust/tests/view-pk-client/src/main.rs +++ b/sdks/rust/tests/view-pk-client/src/main.rs @@ -17,7 +17,6 @@ fn main() { .nth(1) .expect("Pass a test name as a command-line argument to the test client"); let db_name = std::env::var("SPACETIME_SDK_TEST_DB_NAME").expect("Failed to read db name from env"); - // Keep the CLI entrypoint thin so both native and wasm execute the same handlers. tokio::runtime::Runtime::new() .unwrap() diff --git a/sdks/rust/tests/view-pk-client/src/test_handlers.rs b/sdks/rust/tests/view-pk-client/src/test_handlers.rs index 6306d8a2562..e19a5677449 100644 --- a/sdks/rust/tests/view-pk-client/src/test_handlers.rs +++ b/sdks/rust/tests/view-pk-client/src/test_handlers.rs @@ -1,9 +1,7 @@ use crate::module_bindings::*; use spacetimedb_sdk::TableWithPrimaryKey; use spacetimedb_sdk::{error::InternalError, DbConnectionBuilder, DbContext}; -use test_counter::TestCounter; - -const LOCALHOST: &str = "http://localhost:3000"; +use test_counter::{server_url, TestCounter}; type ResultRecorder = Box)>; @@ -55,7 +53,7 @@ async fn connect_then_named( let name = db_name.to_owned(); let conn = DbConnection::builder() .with_database_name(name) - .with_uri(LOCALHOST) + .with_uri(server_url()) .on_connect(|ctx, _, _| { callback(ctx); connected_result(Ok(())); diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 1725f498839..39f0f5a2e42 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -518,6 +518,20 @@ fn main() -> Result<()> { "--test-threads=2", ) .run()?; + // The SDK test harness uses the same child-process server guard as smoketests, + // which expects release CLI/standalone binaries to already exist. + cmd!( + "cargo", + "build", + "--release", + "-p", + "spacetimedb-cli", + "-p", + "spacetimedb-standalone", + "--features", + "spacetimedb-standalone/allow_loopback_http_for_tests", + ) + .run()?; // SDK procedure tests intentionally make localhost HTTP requests. cmd!( "cargo",