From bbfa3d68027bc8df22fc2aa5ffb291b1c6cb7cd5 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 13:46:16 -0400 Subject: [PATCH 01/11] WIP refactor smoketest publish helpers --- crates/smoketests/src/lib.rs | 367 ++++++++++++++++++++++++----------- 1 file changed, 250 insertions(+), 117 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 354f9f0c332..4d1fc9bd2bc 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -515,6 +515,198 @@ impl Default for PublishOptions { } } +impl PublishOptions { + pub fn clear(mut self, clear: bool) -> Self { + self.clear = clear; + self + } + + pub fn break_clients(mut self, break_clients: bool) -> Self { + self.break_clients = break_clients; + self + } + + pub fn num_replicas(mut self, num_replicas: u32) -> Self { + self.num_replicas = Some(num_replicas); + self + } + + pub fn organization(mut self, organization: impl Into) -> Self { + self.organization = Some(organization.into()); + self + } + + pub fn force(mut self, force: bool) -> Self { + self.force = force; + self + } + + pub fn stdin(mut self, stdin_input: impl Into) -> Self { + self.stdin_input = Some(stdin_input.into()); + self + } +} + +pub struct PublishBuilder<'a> { + smoketest: &'a mut Smoketest, + name: Option, + options: PublishOptions, +} + +impl<'a> PublishBuilder<'a> { + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn clear(mut self, clear: bool) -> Self { + self.options.clear = clear; + self + } + + pub fn break_clients(mut self, break_clients: bool) -> Self { + self.options.break_clients = break_clients; + self + } + + pub fn num_replicas(mut self, num_replicas: u32) -> Self { + self.options.num_replicas = Some(num_replicas); + self + } + + pub fn organization(mut self, organization: impl Into) -> Self { + self.options.organization = Some(organization.into()); + self + } + + pub fn force(mut self, force: bool) -> Self { + self.options.force = force; + self + } + + pub fn stdin(mut self, stdin_input: impl Into) -> Self { + self.options.force = false; + self.options.stdin_input = Some(stdin_input.into()); + self + } + + pub fn options(mut self, options: PublishOptions) -> Self { + self.options = options; + self + } + + pub fn current_database(mut self) -> Result { + let identity = self + .smoketest + .database_identity + .as_ref() + .context("No database published yet")? + .clone(); + self.name = Some(identity); + Ok(self) + } + + pub fn run(self) -> Result { + self.smoketest + .publish_module_internal(self.name.as_deref(), self.options) + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ModuleLanguage { + TypeScript, + CSharp, + Cpp, +} + +pub struct ModuleSourcePublishBuilder<'a> { + smoketest: &'a mut Smoketest, + language: ModuleLanguage, + project_dir_name: String, + module_name: String, + module_source: String, + clear: bool, +} + +impl<'a> ModuleSourcePublishBuilder<'a> { + pub fn clear(mut self, clear: bool) -> Self { + self.clear = clear; + self + } + + pub fn run(self) -> Result { + self.smoketest.publish_module_source_internal( + self.language, + &self.project_dir_name, + &self.module_name, + &self.module_source, + self.clear, + ) + } +} + +pub struct SubscribeBuilder<'a> { + smoketest: &'a Smoketest, + database: Option, + queries: Vec, + expected_rows: usize, + confirmed: Option, +} + +impl<'a> SubscribeBuilder<'a> { + pub fn database(mut self, database: impl Into) -> Self { + self.database = Some(database.into()); + self + } + + pub fn expect_rows(mut self, expected_rows: usize) -> Self { + self.expected_rows = expected_rows; + self + } + + pub fn confirmed(mut self, confirmed: bool) -> Self { + self.confirmed = Some(confirmed); + self + } + + pub fn run(self) -> Result> { + let start = Instant::now(); + let owned_identity; + let database = if let Some(database) = self.database.as_deref() { + database + } else { + owned_identity = self + .smoketest + .database_identity + .as_ref() + .context("No database published")? + .clone(); + &owned_identity + }; + let queries = self.queries.iter().map(String::as_str).collect::>(); + self.smoketest + .subscribe_on_impl(database, &queries, self.expected_rows, self.confirmed, start) + } + + pub fn background(self) -> Result { + let owned_identity; + let database = if let Some(database) = self.database.as_deref() { + database + } else { + owned_identity = self + .smoketest + .database_identity + .as_ref() + .context("No database published")? + .clone(); + &owned_identity + }; + let queries = self.queries.iter().map(String::as_str).collect::>(); + self.smoketest + .subscribe_background_on_impl(database, &queries, self.expected_rows, self.confirmed) + } +} + /// Builder for creating `Smoketest` instances. pub struct SmoketestBuilder { module_code: Option, @@ -690,7 +882,7 @@ pub fn noop(_ctx: &ReducerContext) {} } if self.autopublish { - smoketest.publish_module().expect("Failed to publish module"); + smoketest.publish().run().expect("Failed to publish module"); } eprintln!("[TIMING] total build: {:?}", build_start.elapsed()); @@ -904,28 +1096,45 @@ impl Smoketest { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } - /// Initializes, writes, and publishes a TypeScript module from source. - /// - /// Will publish with the `--clear-database` flag. - /// - /// The module is initialized at `//spacetimedb`. - /// On success this updates `self.database_identity`. - pub fn publish_typescript_module_source( + pub fn publish_source( &mut self, + language: ModuleLanguage, + project_dir_name: impl Into, + module_name: impl Into, + module_source: impl Into, + ) -> ModuleSourcePublishBuilder<'_> { + ModuleSourcePublishBuilder { + smoketest: self, + language, + project_dir_name: project_dir_name.into(), + module_name: module_name.into(), + module_source: module_source.into(), + clear: true, + } + } + + fn publish_module_source_internal( + &mut self, + language: ModuleLanguage, project_dir_name: &str, module_name: &str, module_source: &str, + clear: bool, ) -> Result { - self.publish_typescript_module_source_clear(project_dir_name, module_name, module_source, true) + match language { + ModuleLanguage::TypeScript => { + self.publish_typescript_module_source_internal(project_dir_name, module_name, module_source, clear) + } + ModuleLanguage::CSharp => { + self.publish_csharp_module_source_internal(project_dir_name, module_name, module_source, clear) + } + ModuleLanguage::Cpp => { + self.publish_cpp_module_source_internal(project_dir_name, module_name, module_source, clear) + } + } } - /// Initializes, writes, and publishes a TypeScript module from source. - /// - /// If `clear` is `true`, this will publish with the `--clear-database` flag. - /// - /// The module is initialized at `//spacetimedb`. - /// On success this updates `self.database_identity`. - pub fn publish_typescript_module_source_clear( + fn publish_typescript_module_source_internal( &mut self, project_dir_name: &str, module_name: &str, @@ -980,15 +1189,12 @@ impl Smoketest { Ok(identity) } - /// Initializes, writes, and publishes a C# module from source. - /// - /// The module is initialized at `//spacetimedb`. - /// On success this updates `self.database_identity`. - pub fn publish_csharp_module_source( + fn publish_csharp_module_source_internal( &mut self, project_dir_name: &str, module_name: &str, module_source: &str, + clear: bool, ) -> Result { let module_root = self.project_dir.path().join(project_dir_name); let module_root_str = module_root.to_str().context("Invalid C# project path")?; @@ -1007,16 +1213,19 @@ impl Smoketest { csharp::prepare_csharp_module(&module_path)?; let module_path_str = module_path.to_str().context("Invalid C# module path")?; - let publish_output = self.spacetime(&[ + let mut publish_args = vec![ "publish", "--server", &self.server_url, "--module-path", module_path_str, "--yes", - "--clear-database", - module_name, - ])?; + ]; + if clear { + publish_args.push("--clear-database"); + } + publish_args.push(module_name); + let publish_output = self.spacetime(&publish_args)?; csharp::verify_csharp_module_restore(&module_path)?; let identity = parse_identity_from_publish_output(&publish_output)?; @@ -1025,15 +1234,12 @@ impl Smoketest { Ok(identity) } - /// Writes and publishes a C++ module from source. - /// - /// The module is created at `/`. - /// On success this updates `self.database_identity`. - pub fn publish_cpp_module_source( + fn publish_cpp_module_source_internal( &mut self, project_dir_name: &str, module_name: &str, module_source: &str, + clear: bool, ) -> Result { let module_path = self.project_dir.path().join(project_dir_name); let src_dir = module_path.join("src"); @@ -1050,16 +1256,19 @@ impl Smoketest { fs::write(src_dir.join("lib.cpp"), module_source).context("Failed to write C++ module code")?; let module_path_str = module_path.to_str().context("Invalid C++ module path")?; - let publish_output = self.spacetime(&[ + let mut publish_args = vec![ "publish", "--server", &self.server_url, "--module-path", module_path_str, "--yes", - "--clear-database", - module_name, - ])?; + ]; + if clear { + publish_args.push("--clear-database"); + } + publish_args.push(module_name); + let publish_output = self.spacetime(&publish_args)?; let identity = parse_identity_from_publish_output(&publish_output)?; self.database_identity = Some(identity.clone()); @@ -1167,91 +1376,15 @@ log = "0.4" output } - /// Publishes the module and stores the database identity. - pub fn publish_module(&mut self) -> Result { - self.publish_module_internal_ext(None, PublishOptions::default()) - } - - /// Publishes the module with a specific name and optional clear flag. - /// - /// If `name` is provided, the database will be published with that name. - /// If `clear` is true, the database will be cleared before publishing. - pub fn publish_module_named(&mut self, name: &str, clear: bool) -> Result { - self.publish_module_internal_ext( - Some(name), - PublishOptions { - clear, - ..PublishOptions::default() - }, - ) - } - - pub fn publish_module_named_ext(&mut self, name: &str, opts: PublishOptions) -> Result { - self.publish_module_internal_ext(Some(name), opts) - } - - /// Re-publishes the module to the existing database identity with optional clear. - /// - /// This is useful for testing auto-migrations where you want to update - /// the module without clearing the database. - pub fn publish_module_clear(&mut self, clear: bool) -> Result { - let identity = self - .database_identity - .as_ref() - .context("No database published yet")? - .clone(); - self.publish_module_internal_ext( - Some(&identity), - PublishOptions { - clear, - ..PublishOptions::default() - }, - ) - } - - /// Publishes the module with name, clear, and break_clients options. - pub fn publish_module_with_options(&mut self, name: &str, clear: bool, break_clients: bool) -> Result { - self.publish_module_internal_ext( - Some(name), - PublishOptions { - clear, - break_clients, - ..PublishOptions::default() - }, - ) - } - - /// Publishes the module and allows supplying stdin input to the CLI. - /// - /// Useful for interactive publish prompts which require typed acknowledgements. - /// Note: does NOT pass `--yes` so that interactive prompts are not suppressed. - pub fn publish_module_with_stdin(&mut self, name: &str, stdin_input: &str) -> Result { - self.publish_module_internal_ext( - Some(name), - PublishOptions { - force: false, - stdin_input: Some(stdin_input.to_string()), - ..PublishOptions::default() - }, - ) - } - - /// Publishes the module without passing `--yes`, so interactive prompts are not suppressed. - pub fn publish_module_named_no_force(&mut self, name: &str) -> Result { - self.publish_module_internal_ext( - Some(name), - PublishOptions { - force: false, - ..PublishOptions::default() - }, - ) - } - - pub fn publish_module_with_options_ext(&mut self, name: &str, opts: PublishOptions) -> Result { - self.publish_module_internal_ext(Some(name), opts) + pub fn publish(&mut self) -> PublishBuilder<'_> { + PublishBuilder { + smoketest: self, + name: None, + options: PublishOptions::default(), + } } - fn publish_module_internal_ext(&mut self, name: Option<&str>, opts: PublishOptions) -> Result { + fn publish_module_internal(&mut self, name: Option<&str>, opts: PublishOptions) -> Result { let start = Instant::now(); // Determine the WASM path - either precompiled or build it From d6733c9e2236d9b94daae4d7c5eb9643ec266f3d Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 14:47:46 -0400 Subject: [PATCH 02/11] Address smoketest publish builder review --- crates/smoketests/src/lib.rs | 215 +++++++++++++++-------------------- 1 file changed, 91 insertions(+), 124 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 4d1fc9bd2bc..4af60f9d5a8 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -492,65 +492,29 @@ impl ApiResponse { } } -#[derive(Clone, Debug)] -pub struct PublishOptions { - pub clear: bool, - pub break_clients: bool, - pub num_replicas: Option, - pub organization: Option, - pub force: bool, - pub stdin_input: Option, -} - -impl Default for PublishOptions { - fn default() -> Self { - Self { - clear: false, - break_clients: false, - num_replicas: None, - organization: None, - force: true, - stdin_input: None, - } - } +pub struct PublishBuilder<'a> { + smoketest: &'a mut Smoketest, + name: Option, + clear: bool, + break_clients: bool, + num_replicas: Option, + organization: Option, + force: bool, + stdin_input: Option, + source: Option, } -impl PublishOptions { - pub fn clear(mut self, clear: bool) -> Self { - self.clear = clear; - self - } - - pub fn break_clients(mut self, break_clients: bool) -> Self { - self.break_clients = break_clients; - self - } - - pub fn num_replicas(mut self, num_replicas: u32) -> Self { - self.num_replicas = Some(num_replicas); - self - } - - pub fn organization(mut self, organization: impl Into) -> Self { - self.organization = Some(organization.into()); - self - } - - pub fn force(mut self, force: bool) -> Self { - self.force = force; - self - } - - pub fn stdin(mut self, stdin_input: impl Into) -> Self { - self.stdin_input = Some(stdin_input.into()); - self - } +#[derive(Clone, Copy, Debug)] +pub enum ModuleLanguage { + TypeScript, + CSharp, + Cpp, } -pub struct PublishBuilder<'a> { - smoketest: &'a mut Smoketest, - name: Option, - options: PublishOptions, +struct ModuleSource { + language: ModuleLanguage, + project_dir_name: String, + module_source: String, } impl<'a> PublishBuilder<'a> { @@ -560,38 +524,33 @@ impl<'a> PublishBuilder<'a> { } pub fn clear(mut self, clear: bool) -> Self { - self.options.clear = clear; + self.clear = clear; self } pub fn break_clients(mut self, break_clients: bool) -> Self { - self.options.break_clients = break_clients; + self.break_clients = break_clients; self } pub fn num_replicas(mut self, num_replicas: u32) -> Self { - self.options.num_replicas = Some(num_replicas); + self.num_replicas = Some(num_replicas); self } pub fn organization(mut self, organization: impl Into) -> Self { - self.options.organization = Some(organization.into()); + self.organization = Some(organization.into()); self } pub fn force(mut self, force: bool) -> Self { - self.options.force = force; + self.force = force; self } pub fn stdin(mut self, stdin_input: impl Into) -> Self { - self.options.force = false; - self.options.stdin_input = Some(stdin_input.into()); - self - } - - pub fn options(mut self, options: PublishOptions) -> Self { - self.options = options; + self.force = false; + self.stdin_input = Some(stdin_input.into()); self } @@ -606,41 +565,52 @@ impl<'a> PublishBuilder<'a> { Ok(self) } - pub fn run(self) -> Result { - self.smoketest - .publish_module_internal(self.name.as_deref(), self.options) - } -} - -#[derive(Clone, Copy, Debug)] -pub enum ModuleLanguage { - TypeScript, - CSharp, - Cpp, -} - -pub struct ModuleSourcePublishBuilder<'a> { - smoketest: &'a mut Smoketest, - language: ModuleLanguage, - project_dir_name: String, - module_name: String, - module_source: String, - clear: bool, -} - -impl<'a> ModuleSourcePublishBuilder<'a> { - pub fn clear(mut self, clear: bool) -> Self { - self.clear = clear; + pub fn source( + mut self, + language: ModuleLanguage, + project_dir_name: impl Into, + module_source: impl Into, + ) -> Self { + self.source = Some(ModuleSource { + language, + project_dir_name: project_dir_name.into(), + module_source: module_source.into(), + }); self } pub fn run(self) -> Result { - self.smoketest.publish_module_source_internal( - self.language, - &self.project_dir_name, - &self.module_name, - &self.module_source, - self.clear, + let PublishBuilder { + smoketest, + name, + clear, + break_clients, + num_replicas, + organization, + force, + stdin_input, + source, + } = self; + + if let Some(source) = source { + let module_name = name.as_deref().context("No module name provided for source publish")?; + return smoketest.publish_module_source_internal( + source.language, + &source.project_dir_name, + module_name, + &source.module_source, + clear, + ); + } + + smoketest.publish_module_internal( + name.as_deref(), + clear, + break_clients, + num_replicas, + organization.as_deref(), + force, + stdin_input.as_deref(), ) } } @@ -1096,23 +1066,6 @@ impl Smoketest { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } - pub fn publish_source( - &mut self, - language: ModuleLanguage, - project_dir_name: impl Into, - module_name: impl Into, - module_source: impl Into, - ) -> ModuleSourcePublishBuilder<'_> { - ModuleSourcePublishBuilder { - smoketest: self, - language, - project_dir_name: project_dir_name.into(), - module_name: module_name.into(), - module_source: module_source.into(), - clear: true, - } - } - fn publish_module_source_internal( &mut self, language: ModuleLanguage, @@ -1380,11 +1333,26 @@ log = "0.4" PublishBuilder { smoketest: self, name: None, - options: PublishOptions::default(), + clear: false, + break_clients: false, + num_replicas: None, + organization: None, + force: true, + stdin_input: None, + source: None, } } - fn publish_module_internal(&mut self, name: Option<&str>, opts: PublishOptions) -> Result { + fn publish_module_internal( + &mut self, + name: Option<&str>, + clear: bool, + break_clients: bool, + num_replicas: Option, + organization: Option<&str>, + force: bool, + stdin_input: Option<&str>, + ) -> Result { let start = Instant::now(); // Determine the WASM path - either precompiled or build it @@ -1427,26 +1395,25 @@ log = "0.4" let publish_start = Instant::now(); let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; - if opts.force { + if force { args.push("--yes"); } - if opts.clear { + if clear { args.push("--clear-database"); } - if opts.break_clients { + if break_clients { args.push("--break-clients"); } - let num_replicas_owned = opts.num_replicas.map(|n| n.to_string()); + let num_replicas_owned = num_replicas.map(|n| n.to_string()); if let Some(n) = num_replicas_owned.as_ref() { args.push("--num-replicas"); args.push(n); } - let org_owned = opts.organization.clone(); - if let Some(org) = org_owned.as_ref() { + if let Some(org) = organization { args.push("--organization"); args.push(org); } @@ -1457,7 +1424,7 @@ log = "0.4" args.push(&name_owned); } - let output = match opts.stdin_input.as_deref() { + let output = match stdin_input { Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?, None => self.spacetime(&args)?, }; From bf7a05a5d0b30ae20a7c16836d56da9d02d1b772 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 15:12:24 -0400 Subject: [PATCH 03/11] Finish smoketest builder refactor --- crates/smoketests/src/lib.rs | 147 ++++-------------- .../tests/smoketests/add_remove_index.rs | 12 +- .../tests/smoketests/auto_migration.rs | 39 +++-- .../tests/smoketests/change_host_type.rs | 16 +- .../smoketests/tests/smoketests/cli/list.rs | 2 +- .../smoketests/client_connection_errors.rs | 4 +- .../tests/smoketests/confirmed_reads.rs | 5 +- .../tests/smoketests/delete_database.rs | 12 +- crates/smoketests/tests/smoketests/dml.rs | 6 +- crates/smoketests/tests/smoketests/domains.rs | 14 +- .../tests/smoketests/fail_initial_publish.rs | 6 +- .../tests/smoketests/http_routes.rs | 37 ++++- crates/smoketests/tests/smoketests/modules.rs | 18 ++- .../tests/smoketests/new_user_flow.rs | 2 +- .../tests/smoketests/permissions.rs | 14 +- crates/smoketests/tests/smoketests/pg_wire.rs | 6 +- .../smoketests/publish_upgrade_prompt.rs | 16 +- crates/smoketests/tests/smoketests/restart.rs | 22 +-- crates/smoketests/tests/smoketests/rls.rs | 10 +- .../tests/smoketests/schedule_reducer.rs | 16 +- .../tests/smoketests/timestamp_route.rs | 2 +- .../typescript_index_source_name.rs | 25 +-- crates/smoketests/tests/smoketests/views.rs | 132 +++++++++++----- 23 files changed, 312 insertions(+), 251 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 4af60f9d5a8..0bbf122f743 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -517,6 +517,16 @@ struct ModuleSource { module_source: String, } +struct PublishCommand<'a> { + name: Option<&'a str>, + clear: bool, + break_clients: bool, + num_replicas: Option, + organization: Option<&'a str>, + force: bool, + stdin_input: Option<&'a str>, +} + impl<'a> PublishBuilder<'a> { pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); @@ -603,15 +613,15 @@ impl<'a> PublishBuilder<'a> { ); } - smoketest.publish_module_internal( - name.as_deref(), + smoketest.publish_module_internal(PublishCommand { + name: name.as_deref(), clear, break_clients, num_replicas, - organization.as_deref(), + organization: organization.as_deref(), force, - stdin_input.as_deref(), - ) + stdin_input: stdin_input.as_deref(), + }) } } @@ -1343,16 +1353,7 @@ log = "0.4" } } - fn publish_module_internal( - &mut self, - name: Option<&str>, - clear: bool, - break_clients: bool, - num_replicas: Option, - organization: Option<&str>, - force: bool, - stdin_input: Option<&str>, - ) -> Result { + fn publish_module_internal(&mut self, publish: PublishCommand<'_>) -> Result { let start = Instant::now(); // Determine the WASM path - either precompiled or build it @@ -1395,36 +1396,36 @@ log = "0.4" let publish_start = Instant::now(); let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; - if force { + if publish.force { args.push("--yes"); } - if clear { + if publish.clear { args.push("--clear-database"); } - if break_clients { + if publish.break_clients { args.push("--break-clients"); } - let num_replicas_owned = num_replicas.map(|n| n.to_string()); + let num_replicas_owned = publish.num_replicas.map(|n| n.to_string()); if let Some(n) = num_replicas_owned.as_ref() { args.push("--num-replicas"); args.push(n); } - if let Some(org) = organization { + if let Some(org) = publish.organization { args.push("--organization"); args.push(org); } let name_owned; - if let Some(n) = name { + if let Some(n) = publish.name { name_owned = n.to_string(); args.push(&name_owned); } - let output = match stdin_input { + let output = match publish.stdin_input { Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?, None => self.spacetime(&args)?, }; @@ -1698,43 +1699,14 @@ log = "0.4" Ok(ApiResponse { status_code, body }) } - /// Starts a subscription and waits for N updates (synchronous). - /// - /// Returns the updates as JSON values. - /// For tests that need to perform actions while subscribed, use `subscribe_background` instead. - pub fn subscribe(&self, queries: &[&str], n: usize) -> Result> { - self.subscribe_opts(queries, n, None) - } - - pub fn subscribe_on(&self, database: &str, queries: &[&str], n: usize) -> Result> { - self.subscribe_on_opts(database, queries, n, Some(false)) - } - - /// Starts a subscription with --confirmed flag and waits for N updates. - pub fn subscribe_confirmed(&self, queries: &[&str], n: usize) -> Result> { - self.subscribe_opts(queries, n, Some(true)) - } - - pub fn subscribe_on_confirmed(&self, database: &str, queries: &[&str], n: usize) -> Result> { - self.subscribe_on_opts(database, queries, n, Some(true)) - } - - /// Internal helper for subscribe with options. - fn subscribe_opts(&self, queries: &[&str], n: usize, confirmed: Option) -> Result> { - let start = Instant::now(); - let identity = self.database_identity.as_ref().context("No database published")?; - self.subscribe_on_impl(identity, queries, n, confirmed, start) - } - - fn subscribe_on_opts( - &self, - database: &str, - queries: &[&str], - n: usize, - confirmed: Option, - ) -> Result> { - let start = Instant::now(); - self.subscribe_on_impl(database, queries, n, confirmed, start) + pub fn subscribe(&self, queries: &[&str]) -> SubscribeBuilder<'_> { + SubscribeBuilder { + smoketest: self, + database: None, + queries: queries.iter().map(|query| query.to_string()).collect(), + expected_rows: 1, + confirmed: None, + } } fn subscribe_on_impl( @@ -1788,63 +1760,6 @@ log = "0.4" .collect() } - /// Starts a subscription in the background and returns a handle. - /// - /// This matches Python's subscribe semantics - start subscription first, - /// perform actions, then call the handle to collect results. - pub fn subscribe_background(&self, queries: &[&str], n: usize) -> Result { - self.subscribe_background_opts(queries, n, None) - } - - pub fn subscribe_background_on(&self, database: &str, queries: &[&str], n: usize) -> Result { - self.subscribe_background_on_opts(database, queries, n, Some(false)) - } - - /// Starts a subscription in the background with --confirmed flag. - pub fn subscribe_background_confirmed(&self, queries: &[&str], n: usize) -> Result { - self.subscribe_background_opts(queries, n, Some(true)) - } - - /// Starts a subscription in the background with --confirmed flag. - pub fn subscribe_background_unconfirmed(&self, queries: &[&str], n: usize) -> Result { - self.subscribe_background_opts(queries, n, Some(false)) - } - - pub fn subscribe_background_on_confirmed( - &self, - database: &str, - queries: &[&str], - n: usize, - ) -> Result { - self.subscribe_background_on_opts(database, queries, n, Some(true)) - } - - /// Internal helper for background subscribe with options. - fn subscribe_background_opts( - &self, - queries: &[&str], - n: usize, - confirmed: Option, - ) -> Result { - let identity = self - .database_identity - .as_ref() - .context("No database published")? - .clone(); - - self.subscribe_background_on_impl(&identity, queries, n, confirmed) - } - - fn subscribe_background_on_opts( - &self, - database: &str, - queries: &[&str], - n: usize, - confirmed: Option, - ) -> Result { - self.subscribe_background_on_impl(database, queries, n, confirmed) - } - fn subscribe_background_on_impl( &self, database: &str, diff --git a/crates/smoketests/tests/smoketests/add_remove_index.rs b/crates/smoketests/tests/smoketests/add_remove_index.rs index a145374ae3f..9b0261bd8e0 100644 --- a/crates/smoketests/tests/smoketests/add_remove_index.rs +++ b/crates/smoketests/tests/smoketests/add_remove_index.rs @@ -19,17 +19,17 @@ fn test_add_then_remove_index() { // Publish and attempt a subscribing to a join query. // There are no indices, resulting in an unsupported unindexed join. - test.publish_module_named(&name, false).unwrap(); - let result = test.subscribe(&[JOIN_QUERY], 0); + test.publish().name(&name).run().unwrap(); + let result = test.subscribe(&[JOIN_QUERY]).expect_rows(0).run(); assert!(result.is_err(), "Expected subscription to fail without indices"); // Publish the indexed version. // Now we have indices, so the query should be accepted. test.use_precompiled_module("add-remove-index-indexed"); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Subscribe and hold across the call, then collect results - let sub = test.subscribe_background(&[JOIN_QUERY], 1).unwrap(); + let sub = test.subscribe(&[JOIN_QUERY]).expect_rows(1).background().unwrap(); test.call_anon("add", &[]).unwrap(); let results = sub.collect().unwrap(); assert_eq!(results.len(), 1, "Expected 1 update from subscription"); @@ -37,7 +37,7 @@ fn test_add_then_remove_index() { // Publish the unindexed version again, removing the index. // The initial subscription should be rejected again. test.use_precompiled_module("add-remove-index"); - test.publish_module_named(&name, false).unwrap(); - let result = test.subscribe(&[JOIN_QUERY], 0); + test.publish().name(&name).run().unwrap(); + let result = test.subscribe(&[JOIN_QUERY]).expect_rows(0).run(); assert!(result.is_err(), "Expected subscription to fail after removing indices"); } diff --git a/crates/smoketests/tests/smoketests/auto_migration.rs b/crates/smoketests/tests/smoketests/auto_migration.rs index 102a7837eeb..8e00fef9e8a 100644 --- a/crates/smoketests/tests/smoketests/auto_migration.rs +++ b/crates/smoketests/tests/smoketests/auto_migration.rs @@ -50,7 +50,7 @@ fn test_reject_schema_changes() { // Try to update with incompatible schema (adding column without default) test.write_module_code(MODULE_CODE_UPDATED_INCOMPATIBLE).unwrap(); - let result = test.publish_module_clear(false); + let result = test.publish().current_database().unwrap().run(); assert!( result.is_err(), @@ -201,7 +201,11 @@ pub fn print_books(ctx: &ReducerContext, prefix: String) { fn test_add_table_auto_migration() { let mut test = Smoketest::builder().module_code(MODULE_CODE_INIT).build(); - let sub = test.subscribe_background(&["select * from person"], 4).unwrap(); + let sub = test + .subscribe(&["select * from person"]) + .expect_rows(4) + .background() + .unwrap(); // Add initial data test.call("add_person", &["Robert", "Student"]).unwrap(); @@ -228,7 +232,7 @@ fn test_add_table_auto_migration() { // Update module without clearing database test.write_module_code(MODULE_CODE_UPDATED).unwrap(); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); // Add new data with updated schema test.call("add_person", &["Husserl", "Student"]).unwrap(); @@ -347,7 +351,10 @@ fn test_add_table_columns() { // Otherwise, there's a race between module teardown in publish, vs subscribers // getting the row deletion they expect. subs.push( - test.subscribe_background_unconfirmed(&["select * from person"], 5) + test.subscribe(&["select * from person"]) + .expect_rows(5) + .confirmed(false) + .background() .unwrap(), ); } @@ -358,7 +365,7 @@ fn test_add_table_columns() { // First upgrade: add age & mass columns test.write_module_code(MODULE_CODE_ADD_TABLE_COLUMNS_UPDATED).unwrap(); let identity = test.database_identity.clone().unwrap(); - test.publish_module_with_options(&identity, false, true).unwrap(); + test.publish().name(&identity).break_clients(true).run().unwrap(); test.call("print_persons", &["FIRST_UPDATE"]).unwrap(); let logs1 = test.logs(100).unwrap(); @@ -397,7 +404,7 @@ fn test_add_table_columns() { // Second upgrade test.write_module_code(MODULE_CODE_ADD_TABLE_COLUMNS_UPDATED_AGAIN) .unwrap(); - test.publish_module_with_options(&identity, false, true).unwrap(); + test.publish().name(&identity).break_clients(true).run().unwrap(); test.call("print_persons", &["UPDATE_2"]).unwrap(); let logs2 = test.logs(100).unwrap(); @@ -480,12 +487,18 @@ fn test_remove_primary_key_issue_3934() { // Step 2: Remove primary key. Should succeed. test.write_module_code(MODULE_CODE_WITHOUT_PK).unwrap(); - test.publish_module_with_options(&identity, false, true) + test.publish() + .name(&identity) + .break_clients(true) + .run() .expect("Removing primary key should succeed"); // Step 3: Trivial change (add a reducer). This is where #3934 crashes. test.write_module_code(MODULE_CODE_WITHOUT_PK_V2).unwrap(); - test.publish_module_with_options(&identity, false, true) + test.publish() + .name(&identity) + .break_clients(true) + .run() .expect("Publish after PK removal should succeed (issue #3934)"); } @@ -535,11 +548,17 @@ fn automigrate_reschema_event_table_arbitrarily() { // Step 2: Reschema event table. Should work fine, even though we'd reject this change for a non-event table. test.write_module_code(MODULE_CODE_WITH_EVENT_TABLE_AFTER).unwrap(); - test.publish_module_with_options(&identity, false, true) + test.publish() + .name(&identity) + .break_clients(true) + .run() .expect("Changing schema of event table should succeed"); // Step 3: Reschema event table right back. Should still work fine. test.write_module_code(MODULE_CODE_WITH_EVENT_TABLE_BEFORE).unwrap(); - test.publish_module_with_options(&identity, false, true) + test.publish() + .name(&identity) + .break_clients(true) + .run() .expect("Changing schema of event table should succeed"); } diff --git a/crates/smoketests/tests/smoketests/change_host_type.rs b/crates/smoketests/tests/smoketests/change_host_type.rs index b8038975ee6..c4a0a17581e 100644 --- a/crates/smoketests/tests/smoketests/change_host_type.rs +++ b/crates/smoketests/tests/smoketests/change_host_type.rs @@ -1,5 +1,5 @@ use spacetimedb::messages::control_db::HostType; -use spacetimedb_smoketests::{require_local_server, require_pnpm, Smoketest}; +use spacetimedb_smoketests::{require_local_server, require_pnpm, ModuleLanguage, Smoketest}; const TS_MODULE_BASIC: &str = r#"import { schema, t, table } from "spacetimedb/server"; @@ -35,11 +35,14 @@ fn test_update_with_different_host_type() { .autopublish(false) .build(); - let database_identity = test.publish_module().unwrap(); + let database_identity = test.publish().run().unwrap(); add_person(&test, PERSON_A, "initial"); // Publish a TS module. - test.publish_typescript_module_source_clear("modules-basic-ts", &database_identity, TS_MODULE_BASIC, false) + test.publish() + .name(&database_identity) + .source(ModuleLanguage::TypeScript, "modules-basic-ts", TS_MODULE_BASIC) + .run() .unwrap(); add_person(&test, PERSON_B, "post module update"); @@ -48,7 +51,7 @@ fn test_update_with_different_host_type() { assert_has_rows(&test, &[PERSON_A, PERSON_B], "post restart"); // Change back to original module and assert that the data is still there. - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); add_person(&test, PERSON_C, "post revert"); // Restart once more and assert that the data is still there. @@ -99,7 +102,10 @@ fn test_repair_host_type() { let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_typescript_module_source("modules-basic-ts", "basic-ts-change-host-type", TS_MODULE_BASIC) + test.publish() + .name("basic-ts-change-host-type") + .source(ModuleLanguage::TypeScript, "modules-basic-ts", TS_MODULE_BASIC) + .run() .unwrap(); assert_host_type(&test, HostType::Js); // Set the program kind to the wrong value. diff --git a/crates/smoketests/tests/smoketests/cli/list.rs b/crates/smoketests/tests/smoketests/cli/list.rs index 54c03144733..8832a25c181 100644 --- a/crates/smoketests/tests/smoketests/cli/list.rs +++ b/crates/smoketests/tests/smoketests/cli/list.rs @@ -28,7 +28,7 @@ fn cli_list_shows_database_names_and_identities() { let primary_name = format!("list-db-{}", std::process::id()); let alias_name = format!("{primary_name}-alias"); let second_alias_name = format!("{primary_name}-alt"); - let identity = test.publish_module_named(&primary_name, false).unwrap(); + let identity = test.publish().name(&primary_name).run().unwrap(); let json_body = format!(r#"["{}","{}"]"#, alias_name, second_alias_name); let response = test diff --git a/crates/smoketests/tests/smoketests/client_connection_errors.rs b/crates/smoketests/tests/smoketests/client_connection_errors.rs index 7033c56024d..c937d750ce4 100644 --- a/crates/smoketests/tests/smoketests/client_connection_errors.rs +++ b/crates/smoketests/tests/smoketests/client_connection_errors.rs @@ -8,7 +8,7 @@ fn test_client_connected_error_rejects_connection() { .build(); // Subscribe should fail because client_connected returns an error - let result = test.subscribe(&["SELECT * FROM all_u8s"], 0); + let result = test.subscribe(&["SELECT * FROM all_u8s"]).expect_rows(0).run(); assert!( result.is_err(), "Expected subscribe to fail when client_connected returns error" @@ -35,7 +35,7 @@ fn test_client_disconnected_error_still_deletes_st_client() { .build(); // Subscribe should succeed (client_connected returns Ok) - let result = test.subscribe(&["SELECT * FROM all_u8s"], 0); + let result = test.subscribe(&["SELECT * FROM all_u8s"]).expect_rows(0).run(); assert!(result.is_ok(), "Expected subscribe to succeed"); let logs = test.logs(100).unwrap(); diff --git a/crates/smoketests/tests/smoketests/confirmed_reads.rs b/crates/smoketests/tests/smoketests/confirmed_reads.rs index fd2875eb574..b4de29bea5a 100644 --- a/crates/smoketests/tests/smoketests/confirmed_reads.rs +++ b/crates/smoketests/tests/smoketests/confirmed_reads.rs @@ -11,7 +11,10 @@ fn test_confirmed_reads_receive_updates() { // Start subscription in background with confirmed flag let sub = test - .subscribe_background_confirmed(&["SELECT * FROM person"], 2) + .subscribe(&["SELECT * FROM person"]) + .expect_rows(2) + .confirmed(true) + .background() .unwrap(); // Insert via reducer diff --git a/crates/smoketests/tests/smoketests/delete_database.rs b/crates/smoketests/tests/smoketests/delete_database.rs index 597a9d6e355..2ba01355789 100644 --- a/crates/smoketests/tests/smoketests/delete_database.rs +++ b/crates/smoketests/tests/smoketests/delete_database.rs @@ -21,7 +21,7 @@ fn test_delete_database_aborts_without_confirmation() { .build(); let name = format!("delete-db-abort-{}", std::process::id()); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); let output = test .spacetime(&["delete", "--server", &test.server_url, &name]) @@ -43,11 +43,15 @@ fn test_delete_database_with_confirmation() { .build(); let name = format!("delete-database-{}", std::process::id()); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Start subscription in background to collect updates // We request many updates but will stop early when we delete the db - let sub = test.subscribe_background(&["SELECT * FROM counter"], 1000).unwrap(); + let sub = test + .subscribe(&["SELECT * FROM counter"]) + .expect_rows(1000) + .background() + .unwrap(); // Let the scheduled reducer run for a bit thread::sleep(Duration::from_secs(2)); @@ -78,7 +82,7 @@ fn test_delete_database_yes_skips_confirmation() { .build(); let name = format!("delete-db-yes-{}", std::process::id()); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); let output = test .spacetime(&["delete", "--server", &test.server_url, "--yes", &name]) diff --git a/crates/smoketests/tests/smoketests/dml.rs b/crates/smoketests/tests/smoketests/dml.rs index 2483746238b..77651e74e92 100644 --- a/crates/smoketests/tests/smoketests/dml.rs +++ b/crates/smoketests/tests/smoketests/dml.rs @@ -9,7 +9,11 @@ fn test_subscribe() { let test = Smoketest::builder().precompiled_module("dml").build(); // Start subscription FIRST (in background), matching Python semantics - let sub = test.subscribe_background(&["SELECT * FROM t"], 2).unwrap(); + let sub = test + .subscribe(&["SELECT * FROM t"]) + .expect_rows(2) + .background() + .unwrap(); // Small delay to ensure subscription is connected before inserts thread::sleep(Duration::from_millis(500)); diff --git a/crates/smoketests/tests/smoketests/domains.rs b/crates/smoketests/tests/smoketests/domains.rs index db9f8c2044f..c04440ded2a 100644 --- a/crates/smoketests/tests/smoketests/domains.rs +++ b/crates/smoketests/tests/smoketests/domains.rs @@ -6,7 +6,7 @@ fn test_set_name() { let mut test = Smoketest::builder().autopublish(false).build(); let orig_name = format!("domains-set-name-{}", std::process::id()); - test.publish_module_named(&orig_name, false).unwrap(); + test.publish().name(&orig_name).run().unwrap(); let rand_name = format!("domains-set-name-{}-renamed", std::process::id()); @@ -34,16 +34,16 @@ fn test_subdomain_behavior() { let mut test = Smoketest::builder().autopublish(false).build(); let root_name = format!("domains-subdomain-behavior-{}", std::process::id()); - test.publish_module_named(&root_name, false).unwrap(); + test.publish().name(&root_name).run().unwrap(); // Double slash should fail let double_slash_name = format!("{}//test", root_name); - let result = test.publish_module_named(&double_slash_name, false); + let result = test.publish().name(&double_slash_name).run(); assert!(result.is_err(), "Expected publish to fail with double slash in name"); // Trailing slash should fail let trailing_slash_name = format!("{}/test/", root_name); - let result = test.publish_module_named(&trailing_slash_name, false); + let result = test.publish().name(&trailing_slash_name).run(); assert!(result.is_err(), "Expected publish to fail with trailing slash in name"); } @@ -53,12 +53,12 @@ fn test_set_to_existing_name() { let mut test = Smoketest::builder().autopublish(false).build(); // Publish first database (no name) - test.publish_module().unwrap(); + test.publish().run().unwrap(); let id_to_rename = test.database_identity.clone().unwrap(); // Publish second database with a name let rename_to = format!("domains-set-existing-target-{}", std::process::id()); - test.publish_module_named(&rename_to, false).unwrap(); + test.publish().name(&rename_to).run().unwrap(); // Try to rename first db to the name of the second - should fail let result = test.spacetime(&[ @@ -83,7 +83,7 @@ fn test_replace_names() { let orig_name = format!("domains-replace-names-{}", std::process::id()); let alt_name1 = format!("domains-replace-names-{}-alt1", std::process::id()); let alt_name2 = format!("domains-replace-names-{}-alt2", std::process::id()); - test.publish_module_named(&orig_name, false).unwrap(); + test.publish().name(&orig_name).run().unwrap(); // Use the API to replace names let json_body = format!(r#"["{}","{}"]"#, alt_name1, alt_name2); diff --git a/crates/smoketests/tests/smoketests/fail_initial_publish.rs b/crates/smoketests/tests/smoketests/fail_initial_publish.rs index 3d3308ab792..4d8664cfa51 100644 --- a/crates/smoketests/tests/smoketests/fail_initial_publish.rs +++ b/crates/smoketests/tests/smoketests/fail_initial_publish.rs @@ -27,7 +27,7 @@ fn test_fail_initial_publish() { let name = format!("fail-initial-publish-{}", std::process::id()); // First publish should fail due to broken module - let result = test.publish_module_named(&name, false); + let result = test.publish().name(&name).run(); assert!(result.is_err(), "Expected publish to fail with broken module"); // Describe should fail because database doesn't exist @@ -47,7 +47,7 @@ fn test_fail_initial_publish() { // This used to be broken; the failed initial publish would leave // the control database in a bad state. test.use_precompiled_module("fail-initial-publish-fixed"); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); let describe_output = test .spacetime(&["describe", "--server", &test.server_url, "--json", &name]) @@ -61,7 +61,7 @@ fn test_fail_initial_publish() { // Publishing the broken code again fails, but the database still exists afterwards, // with the previous version of the module code. test.write_module_code(MODULE_CODE_BROKEN).unwrap(); - let result = test.publish_module_named(&name, false); + let result = test.publish().name(&name).run(); assert!(result.is_err(), "Expected publish to fail with broken module"); // Database should still exist with the fixed code diff --git a/crates/smoketests/tests/smoketests/http_routes.rs b/crates/smoketests/tests/smoketests/http_routes.rs index 567c3684ac8..2d8c28b2f1f 100644 --- a/crates/smoketests/tests/smoketests/http_routes.rs +++ b/crates/smoketests/tests/smoketests/http_routes.rs @@ -1,5 +1,7 @@ use regex::Regex; -use spacetimedb_smoketests::{require_dotnet, require_emscripten, require_pnpm, workspace_root, Smoketest}; +use spacetimedb_smoketests::{ + require_dotnet, require_emscripten, require_pnpm, workspace_root, ModuleLanguage, Smoketest, +}; use std::{fs, path::Path}; const MODULE_CODE: &str = r#" @@ -1095,20 +1097,35 @@ fn rust_http_test(module_code: &str) -> (Smoketest, String) { fn cpp_http_test(name: &str, module_code: &str) -> (Smoketest, String) { require_emscripten!(); let mut test = Smoketest::builder().autopublish(false).build(); - let identity = test.publish_cpp_module_source(name, name, module_code).unwrap(); + let identity = test + .publish() + .name(name) + .source(ModuleLanguage::Cpp, name, module_code) + .run() + .unwrap(); (test, identity) } fn typescript_http_test(name: &str, module_code: &str) -> (Smoketest, String) { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); - let identity = test.publish_typescript_module_source(name, name, module_code).unwrap(); + let identity = test + .publish() + .name(name) + .source(ModuleLanguage::TypeScript, name, module_code) + .run() + .unwrap(); (test, identity) } fn csharp_http_test(name: &str, module_code: &str) -> (Smoketest, String) { let mut test = Smoketest::builder().autopublish(false).build(); - let identity = test.publish_csharp_module_source(name, name, module_code).unwrap(); + let identity = test + .publish() + .name(name) + .source(ModuleLanguage::CSharp, name, module_code) + .run() + .unwrap(); (test, identity) } @@ -1528,7 +1545,10 @@ fn cpp_http_handlers_tutorial_say_hello_route_works() { ); let mut test = Smoketest::builder().autopublish(false).build(); let identity = test - .publish_cpp_module_source("http-handlers-docs-cpp", "http-handlers-docs-cpp", &module_code) + .publish() + .name("http-handlers-docs-cpp") + .source(ModuleLanguage::Cpp, "http-handlers-docs-cpp", &module_code) + .run() .unwrap(); let url = format!("{}/v1/database/{identity}/route/say-hello", test.server_url); @@ -1551,11 +1571,14 @@ fn typescript_http_handlers_tutorial_say_hello_route_works() { ); let mut test = Smoketest::builder().autopublish(false).build(); let identity = test - .publish_typescript_module_source( - "http-handlers-docs-typescript", + .publish() + .name("http-handlers-docs-typescript") + .source( + ModuleLanguage::TypeScript, "http-handlers-docs-typescript", &module_code, ) + .run() .unwrap(); let url = format!("{}/v1/database/{identity}/route/say-hello", test.server_url); diff --git a/crates/smoketests/tests/smoketests/modules.rs b/crates/smoketests/tests/smoketests/modules.rs index 8d7c298ae60..f086249085b 100644 --- a/crates/smoketests/tests/smoketests/modules.rs +++ b/crates/smoketests/tests/smoketests/modules.rs @@ -11,7 +11,7 @@ fn test_module_update() { let name = format!("module-update-{}", std::process::id()); // Initial publish - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); test.call("add", &["Robert"]).unwrap(); test.call("add", &["Julie"]).unwrap(); @@ -25,11 +25,11 @@ fn test_module_update() { assert!(logs.iter().any(|l| l.contains("Hello, World!"))); // Unchanged module is ok - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Changing an existing table isn't (adds age column to Person) test.use_precompiled_module("modules-breaking"); - let result = test.publish_module_named(&name, false); + let result = test.publish().name(&name).run(); assert!(result.is_err(), "Expected publish to fail with breaking change"); let err = result.unwrap_err().to_string(); assert!( @@ -43,7 +43,7 @@ fn test_module_update() { // Adding a table is ok test.use_precompiled_module("modules-add-table"); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); test.call("are_we_updated_yet", &[]).unwrap(); let logs = test.logs(2).unwrap(); @@ -103,15 +103,19 @@ fn test_hotswap_module() { let name = format!("hotswap-module-{}", std::process::id()); // Publish initial module and subscribe to all - test.publish_module_named(&name, false).unwrap(); - let sub = test.subscribe_background(&["SELECT * FROM *"], 2).unwrap(); + test.publish().name(&name).run().unwrap(); + let sub = test + .subscribe(&["SELECT * FROM *"]) + .expect_rows(2) + .background() + .unwrap(); // Trigger event on the subscription test.call("add_person", &["Horst"]).unwrap(); // Update the module (adds Pet table) test.use_precompiled_module("hotswap-updated"); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Assert that the module was updated test.call("add_pet", &["Turtle"]).unwrap(); diff --git a/crates/smoketests/tests/smoketests/new_user_flow.rs b/crates/smoketests/tests/smoketests/new_user_flow.rs index a153e44c3c2..21d7a7abdd8 100644 --- a/crates/smoketests/tests/smoketests/new_user_flow.rs +++ b/crates/smoketests/tests/smoketests/new_user_flow.rs @@ -11,7 +11,7 @@ fn test_new_user_flow() { // Create a new identity and publish test.new_identity().unwrap(); - test.publish_module().unwrap(); + test.publish().run().unwrap(); // Calling our database test.call("say_hello", &[]).unwrap(); diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index f0fca600f55..d1c9c92b19f 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -103,7 +103,7 @@ fn test_replace_names() { .build(); let name = format!("permissions-replace-names-{}", std::process::id()); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Switch to a new identity test.new_identity().unwrap(); @@ -141,7 +141,7 @@ fn test_private_table() { assert!(result.is_err(), "Expected query on private table to fail for non-owner"); // Subscribing to the private table fails - let result = test.subscribe(&["SELECT * FROM secret"], 0); + let result = test.subscribe(&["SELECT * FROM secret"]).expect_rows(0).run(); assert!( result.is_err(), "Expected subscribe to private table to fail for non-owner" @@ -149,7 +149,9 @@ fn test_private_table() { // Subscribing to the public table works let sub = test - .subscribe_background(&["SELECT * FROM common_knowledge"], 1) + .subscribe(&["SELECT * FROM common_knowledge"]) + .expect_rows(1) + .background() .unwrap(); test.call("do_thing", &["godmorgon"]).unwrap(); let events = sub.collect().unwrap(); @@ -165,7 +167,11 @@ fn test_private_table() { ); // Subscribing to both tables returns updates for the public one only - let sub = test.subscribe_background(&["SELECT * FROM *"], 1).unwrap(); + let sub = test + .subscribe(&["SELECT * FROM *"]) + .expect_rows(1) + .background() + .unwrap(); test.call("do_thing", &["howdy"]).unwrap(); let events = sub.collect().unwrap(); diff --git a/crates/smoketests/tests/smoketests/pg_wire.rs b/crates/smoketests/tests/smoketests/pg_wire.rs index 94cbcf09e3c..4c69b475aee 100644 --- a/crates/smoketests/tests/smoketests/pg_wire.rs +++ b/crates/smoketests/tests/smoketests/pg_wire.rs @@ -14,7 +14,7 @@ fn test_sql_format() { .autopublish(false) .build(); - test.publish_module_named("pgwire-sql-format", true).unwrap(); + test.publish().name("pgwire-sql-format").clear(true).run().unwrap(); test.call("test", &[]).unwrap(); test.assert_psql( @@ -86,7 +86,7 @@ fn test_sql_conn() { .autopublish(false) .build(); - test.publish_module_named("pgwire-sql-conn", true).unwrap(); + test.publish().name("pgwire-sql-conn").clear(true).run().unwrap(); test.call("test", &[]).unwrap(); let token = test.read_token().unwrap(); @@ -158,7 +158,7 @@ fn test_failures() { .autopublish(false) .build(); - test.publish_module_named("pgwire-failure", true).unwrap(); + test.publish().name("pgwire-failure").clear(true).run().unwrap(); // Empty query returns empty result let output = test.psql("pgwire-failure", "").unwrap(); diff --git a/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs b/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs index ede82c11132..4e39555b91d 100644 --- a/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs +++ b/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs @@ -27,17 +27,23 @@ fn upgrade_prompt_on_publish() { let db_name = format!("upgrade-smoke-{}", random_string()); test.use_precompiled_wasm_path(&old_wasm).unwrap(); - let initial_identity = test.publish_module_named(&db_name, false).unwrap(); + let initial_identity = test.publish().name(&db_name).run().unwrap(); assert_eq!(test.database_identity.as_deref(), Some(initial_identity.as_str())); // Switch back to source-built module, which uses current bindings. test.write_module_code(MODULE_CODE).unwrap(); - let deny_err = test.publish_module_named_no_force(&db_name).unwrap_err().to_string(); + let deny_err = test + .publish() + .name(&db_name) + .force(false) + .run() + .unwrap_err() + .to_string(); assert!(deny_err.contains("major version upgrade from 1.0 to 2.0")); assert!(deny_err.contains("Please type 'upgrade' to accept this change:")); - let accepted_identity = test.publish_module_with_stdin(&db_name, "upgrade\n").unwrap(); + let accepted_identity = test.publish().name(&db_name).stdin("upgrade\n").run().unwrap(); assert_eq!(accepted_identity, initial_identity); } @@ -51,13 +57,13 @@ fn upgrade_prompt_suppressed_by_yes_flag() { let db_name = format!("upgrade-smoke-yes-{}", random_string()); test.use_precompiled_wasm_path(&old_wasm).unwrap(); - let initial_identity = test.publish_module_named(&db_name, false).unwrap(); + let initial_identity = test.publish().name(&db_name).run().unwrap(); assert_eq!(test.database_identity.as_deref(), Some(initial_identity.as_str())); // Switch back to source-built module, which uses current bindings. test.write_module_code(MODULE_CODE).unwrap(); // With --yes, the upgrade prompt should be suppressed and publish should succeed. - let accepted_identity = test.publish_module_named(&db_name, false).unwrap(); + let accepted_identity = test.publish().name(&db_name).run().unwrap(); assert_eq!(accepted_identity, initial_identity); } diff --git a/crates/smoketests/tests/smoketests/restart.rs b/crates/smoketests/tests/smoketests/restart.rs index 2372fd5e487..0bea1a7b340 100644 --- a/crates/smoketests/tests/smoketests/restart.rs +++ b/crates/smoketests/tests/smoketests/restart.rs @@ -90,10 +90,14 @@ fn test_restart_auto_disconnect() { // Start two subscribers in the background let sub1 = test - .subscribe_background(&["SELECT * FROM connected_client"], 2) + .subscribe(&["SELECT * FROM connected_client"]) + .expect_rows(2) + .background() .unwrap(); let sub2 = test - .subscribe_background(&["SELECT * FROM connected_client"], 2) + .subscribe(&["SELECT * FROM connected_client"]) + .expect_rows(2) + .background() .unwrap(); // Call print_num_connected and check we have 3 clients (2 subscribers + the call) @@ -145,8 +149,8 @@ fn test_add_remove_index_after_restart() { // Publish and attempt subscribing to a join query. // There are no indices, resulting in an unsupported unindexed join. - test.publish_module_named(&name, false).unwrap(); - let result = test.subscribe(&[JOIN_QUERY], 0); + test.publish().name(&name).run().unwrap(); + let result = test.subscribe(&[JOIN_QUERY]).expect_rows(0).run(); assert!(result.is_err(), "Expected subscription to fail without indices"); // Restart before adding indices @@ -155,10 +159,10 @@ fn test_add_remove_index_after_restart() { // Publish the indexed version. // Now we have indices, so the query should be accepted. test.use_precompiled_module("add-remove-index-indexed"); - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Subscription should work now - let result = test.subscribe(&[JOIN_QUERY], 0); + let result = test.subscribe(&[JOIN_QUERY]).expect_rows(0).run(); assert!( result.is_ok(), "Expected subscription to succeed with indices, got: {:?}", @@ -166,7 +170,7 @@ fn test_add_remove_index_after_restart() { ); // Verify call works too - let sub = test.subscribe_background(&[JOIN_QUERY], 1).unwrap(); + let sub = test.subscribe(&[JOIN_QUERY]).expect_rows(1).background().unwrap(); test.call_anon("add", &[]).unwrap(); let results = sub.collect().unwrap(); assert_eq!(results.len(), 1, "Expected 1 update from subscription"); @@ -177,7 +181,7 @@ fn test_add_remove_index_after_restart() { // Publish the unindexed version again, removing the index. // The initial subscription should be rejected again. test.use_precompiled_module("add-remove-index"); - test.publish_module_named(&name, false).unwrap(); - let result = test.subscribe(&[JOIN_QUERY], 0); + test.publish().name(&name).run().unwrap(); + let result = test.subscribe(&[JOIN_QUERY]).expect_rows(0).run(); assert!(result.is_err(), "Expected subscription to fail after removing indices"); } diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index 5ed4ded348b..1e309c26700 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -53,7 +53,7 @@ fn test_publish_fails_for_rls_on_private_table() { let name = format!("rls-rules-{}", std::process::id()); // Publishing should fail because RLS is on a private table - let result = test.publish_module_named(&name, false); + let result = test.publish().name(&name).run(); assert!(result.is_err(), "Expected publish to fail for RLS on private table"); } @@ -68,11 +68,11 @@ fn test_rls_disconnect_if_change() { let name = format!("rls-disconnect-{}", std::process::id()); // Initial publish without RLS - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Now re-publish with RLS added (requires --break-clients) test.use_precompiled_module("rls-with-filter"); - test.publish_module_with_options(&name, false, true).unwrap(); + test.publish().name(&name).break_clients(true).run().unwrap(); // Check the row-level SQL filter is added correctly test.assert_sql( @@ -103,10 +103,10 @@ fn test_rls_no_disconnect() { let name = format!("rls-no-disconnect-{}", std::process::id()); // Initial publish with RLS - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // Re-publish the same module (no RLS change) - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); let logs = test.logs(100).unwrap(); diff --git a/crates/smoketests/tests/smoketests/schedule_reducer.rs b/crates/smoketests/tests/smoketests/schedule_reducer.rs index 29172047ae6..f78d6eda0b7 100644 --- a/crates/smoketests/tests/smoketests/schedule_reducer.rs +++ b/crates/smoketests/tests/smoketests/schedule_reducer.rs @@ -21,7 +21,7 @@ fn interval_row_entry(scheduled_id: u64, duration_micros: u64) -> Value { } fn collect_updates_after_call(test: &Smoketest, queries: &[&str], reducer_or_procedure: &str) -> Vec { - let sub = test.subscribe_background(queries, 2).unwrap(); + let sub = test.subscribe(queries).expect_rows(2).background().unwrap(); test.call(reducer_or_procedure, &[]).unwrap(); sub.collect().unwrap() } @@ -98,7 +98,9 @@ fn test_scheduled_table_subscription_repeated_reducer() { // Subscribe to empty scheduled_table. let sub = test - .subscribe_background(&["SELECT * FROM scheduled_table"], 2) + .subscribe(&["SELECT * FROM scheduled_table"]) + .expect_rows(2) + .background() .unwrap(); // Call a reducer to schedule a repeated reducer @@ -198,7 +200,9 @@ fn test_scheduled_procedure_table_subscription_repeated_procedure() { let test = Smoketest::builder().precompiled_module("schedule-procedure").build(); let sub = test - .subscribe_background(&["SELECT * FROM scheduled_table"], 2) + .subscribe(&["SELECT * FROM scheduled_table"]) + .expect_rows(2) + .background() .unwrap(); test.call("schedule_repeated_procedure", &[]).unwrap(); @@ -232,7 +236,11 @@ fn test_scheduled_procedure_table_subscription_repeated_procedure() { fn test_volatile_nonatomic_schedule_immediate() { let test = Smoketest::builder().precompiled_module("schedule-volatile").build(); - let sub = test.subscribe_background(&["SELECT * FROM my_table"], 2).unwrap(); + let sub = test + .subscribe(&["SELECT * FROM my_table"]) + .expect_rows(2) + .background() + .unwrap(); // Insert directly first test.call("do_insert", &[r#""yay!""#]).unwrap(); diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 3130b9c5746..1d337bed1f0 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -23,7 +23,7 @@ fn test_timestamp_route() { ); // Publish a module with the random name - test.publish_module_named(&name, false).unwrap(); + test.publish().name(&name).run().unwrap(); // A request for the timestamp at an extant database is a success let resp = test diff --git a/crates/smoketests/tests/smoketests/typescript_index_source_name.rs b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs index 8c550f81aeb..416ef704faa 100644 --- a/crates/smoketests/tests/smoketests/typescript_index_source_name.rs +++ b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::{random_string, require_local_server, require_pnpm, Smoketest}; +use spacetimedb_smoketests::{random_string, require_local_server, require_pnpm, ModuleLanguage, Smoketest}; const TYPESCRIPT_MODULE_WITHOUT_NEW_COLUMNS: &str = r#"import { schema, table, t } from "spacetimedb/server"; @@ -81,24 +81,29 @@ fn test_typescript_add_optional_columns() { let module_name = format!("typescript-add-optional-columns-{}", random_string()); let database_identity = test - .publish_typescript_module_source( + .publish() + .name(&module_name) + .source( + ModuleLanguage::TypeScript, "typescript-add-optional-columns-v1", - &module_name, TYPESCRIPT_MODULE_WITHOUT_NEW_COLUMNS, ) + .run() .unwrap(); test.call("insert_user", &["Alice", "alice@example.com"]).unwrap(); test.restart_server(); - test.publish_typescript_module_source_clear( - "typescript-add-optional-columns-v2", - &database_identity, - TYPESCRIPT_MODULE_WITH_NEW_COLUMNS, - false, - ) - .unwrap(); + test.publish() + .name(&database_identity) + .source( + ModuleLanguage::TypeScript, + "typescript-add-optional-columns-v2", + TYPESCRIPT_MODULE_WITH_NEW_COLUMNS, + ) + .run() + .unwrap(); test.call("find_user_by_email", &["alice@example.com"]).unwrap(); test.call("find_users_by_active_status", &["false"]).unwrap(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index deb9480997b..460498a03c7 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -1,5 +1,5 @@ use serde_json::{json, Value}; -use spacetimedb_smoketests::{require_dotnet, require_pnpm, Smoketest}; +use spacetimedb_smoketests::{require_dotnet, require_pnpm, ModuleLanguage, Smoketest}; const TS_VIEWS_SUBSCRIBE_MODULE: &str = r#"import { schema, t, table } from "spacetimedb/server"; @@ -220,7 +220,7 @@ fn project_fields(events: Vec, view_name: &str, projected_fields: &[&str] fn assert_count_view_refresh_behavior(test: &Smoketest, view_name: &str, id: &str, value: &str, updated_value: &str) { let query = format!("select * from {view_name}"); - let sub = test.subscribe_background(&[&query], 2).unwrap(); + let sub = test.subscribe(&[&query]).expect_rows(2).background().unwrap(); test.call("insert_item", &[id, value]).unwrap(); test.call("replace_item", &[id, updated_value]).unwrap(); @@ -272,7 +272,7 @@ fn test_fail_publish_namespace_collision() { .autopublish(false) .build(); - let result = test.publish_module(); + let result = test.publish().run(); assert!( result.is_err(), "Expected publish to fail when table and view have same name" @@ -288,7 +288,7 @@ fn test_fail_publish_wrong_return_type() { .autopublish(false) .build(); - let result = test.publish_module(); + let result = test.publish().run(); assert!( result.is_err(), "Expected publish to fail when view return type is not a product type" @@ -484,7 +484,7 @@ fn test_views_auto_migration() { ); test.use_precompiled_module("views-auto-migrate-updated"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); test.assert_sql( "SELECT * FROM player", @@ -504,11 +504,15 @@ fn test_views_auto_migration_stable() { test.call("seed", &[]).unwrap(); // Subscribe to that row through the `z_counter` view - let sub = test.subscribe_background(&["SELECT * FROM z_counter"], 1).unwrap(); + let sub = test + .subscribe(&["SELECT * FROM z_counter"]) + .expect_rows(1) + .background() + .unwrap(); // Update the module by adding another view test.use_precompiled_module("views-auto-migrate-stable-updated"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); // Bump the counter and observe a subscription update. let output = test.call_output("bump_counter", &[]); @@ -545,10 +549,14 @@ fn test_views_auto_migration_read_set() { test.call("seed", &[]).unwrap(); - let _sub = test.subscribe_background(&["SELECT * FROM switched"], 2).unwrap(); + let _sub = test + .subscribe(&["SELECT * FROM switched"]) + .expect_rows(2) + .background() + .unwrap(); test.use_precompiled_module("views-auto-migrate-read-set-updated"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); let logs_after_publish = test.logs(100).unwrap(); let logs_after_publish_for_view = logs_after_publish @@ -575,14 +583,14 @@ fn test_views_auto_migration_read_set() { fn test_auto_migration_drop_view() { let mut test = Smoketest::builder().precompiled_module("views-auto-migrate").build(); test.use_precompiled_module("views-drop-view"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); } #[test] fn test_auto_migration_add_view() { let mut test = Smoketest::builder().precompiled_module("views-drop-view").build(); test.use_precompiled_module("views-auto-migrate"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); } #[test] @@ -592,12 +600,15 @@ fn test_view_primary_key_auto_migration_disconnects_clients() { .build(); let sub = test - .subscribe_background_unconfirmed(&["select * from player"], 2) + .subscribe(&["select * from player"]) + .expect_rows(2) + .confirmed(false) + .background() .unwrap(); test.use_precompiled_module("views-primary-key-auto-migrate-updated"); let identity = test.database_identity.clone().unwrap(); - test.publish_module_with_options(&identity, false, true).unwrap(); + test.publish().name(&identity).break_clients(true).run().unwrap(); sub.collect().unwrap(); @@ -644,7 +655,7 @@ fn test_recovery_from_trapped_views_auto_migration() { ); test.use_precompiled_module("views-trapped"); - let result = test.publish_module_clear(false); + let result = test.publish().current_database().unwrap().run(); assert!(result.is_err(), "Expected trapped publish to fail"); test.assert_sql( @@ -655,7 +666,7 @@ fn test_recovery_from_trapped_views_auto_migration() { ); test.use_precompiled_module("views-recovered"); - test.publish_module_clear(false).unwrap(); + test.publish().current_database().unwrap().run().unwrap(); test.assert_sql( "SELECT * FROM player", @@ -672,7 +683,11 @@ fn test_subscribing_with_different_identities() { test.new_identity().unwrap(); - let sub = test.subscribe_background(&["select * from my_player"], 2).unwrap(); + let sub = test + .subscribe(&["select * from my_player"]) + .expect_rows(2) + .background() + .unwrap(); test.call("insert_player", &["Bob"]).unwrap(); let events = sub.collect().unwrap(); @@ -751,7 +766,11 @@ fn test_where_expr_view() { #[test] fn test_procedure_triggers_subscription_updates() { let test = Smoketest::builder().precompiled_module("views-subscribe").build(); - let sub = test.subscribe_background(&["select * from my_player"], 1).unwrap(); + let sub = test + .subscribe(&["select * from my_player"]) + .expect_rows(1) + .background() + .unwrap(); test.call("insert_player_proc", &["Alice"]).unwrap(); let events = sub.collect().unwrap(); let projection = project_fields(events, "my_player", &["name"]); @@ -767,14 +786,21 @@ fn test_procedure_triggers_subscription_updates() { fn test_typescript_procedure_triggers_subscription_updates() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_typescript_module_source( - "views-subscribe-typescript", - "views-subscribe-typescript", - TS_VIEWS_SUBSCRIBE_MODULE, - ) - .unwrap(); - - let sub = test.subscribe_background(&["select * from my_player"], 1).unwrap(); + test.publish() + .name("views-subscribe-typescript") + .source( + ModuleLanguage::TypeScript, + "views-subscribe-typescript", + TS_VIEWS_SUBSCRIBE_MODULE, + ) + .run() + .unwrap(); + + let sub = test + .subscribe(&["select * from my_player"]) + .expect_rows(1) + .background() + .unwrap(); test.call("insert_player_proc", &["Alice"]).unwrap(); let events = sub.collect().unwrap(); @@ -798,7 +824,10 @@ fn test_csharp_count_view_subscription_refreshes() { require_dotnet!(); let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_csharp_module_source("views-count-csharp", "views-count-csharp", CS_COUNT_VIEW_MODULE) + test.publish() + .name("views-count-csharp") + .source(ModuleLanguage::CSharp, "views-count-csharp", CS_COUNT_VIEW_MODULE) + .run() .unwrap(); assert_all_count_view_refreshes(&test); @@ -809,7 +838,14 @@ fn test_typescript_count_view_subscription_refreshes() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_typescript_module_source("views-count-typescript", "views-count-typescript", TS_COUNT_VIEW_MODULE) + test.publish() + .name("views-count-typescript") + .source( + ModuleLanguage::TypeScript, + "views-count-typescript", + TS_COUNT_VIEW_MODULE, + ) + .run() .unwrap(); assert_all_count_view_refreshes(&test); @@ -822,8 +858,16 @@ fn test_disconnect_does_not_break_sender_view() { test.call("set_player_state", &["42", "1"]).unwrap(); // Two connections subscribe to the same view. - let sub_keep = test.subscribe_background(&["SELECT * FROM player"], 2).unwrap(); - let sub_drop = test.subscribe_background(&["SELECT * FROM player"], 1).unwrap(); + let sub_keep = test + .subscribe(&["SELECT * FROM player"]) + .expect_rows(2) + .background() + .unwrap(); + let sub_drop = test + .subscribe(&["SELECT * FROM player"]) + .expect_rows(1) + .background() + .unwrap(); // Both connections should receive the first update. // After one connection disconnects, the other should still receive updates. @@ -854,10 +898,14 @@ fn test_disconnect_does_not_break_anonymous_view() { // Two connections subscribe to the same anonymous view. let sub_keep = test - .subscribe_background(&["SELECT * FROM player_and_level"], 2) + .subscribe(&["SELECT * FROM player_and_level"]) + .expect_rows(2) + .background() .unwrap(); let sub_drop = test - .subscribe_background(&["SELECT * FROM player_and_level"], 1) + .subscribe(&["SELECT * FROM player_and_level"]) + .expect_rows(1) + .background() .unwrap(); // Both connections should receive the first update. @@ -888,12 +936,15 @@ fn test_disconnect_does_not_break_anonymous_view() { fn test_typescript_query_builder_view_query() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_typescript_module_source( - "views-query-builder-typescript", - "views-query-builder-typescript", - TS_VIEWS_SUBSCRIBE_MODULE, - ) - .unwrap(); + test.publish() + .name("views-query-builder-typescript") + .source( + ModuleLanguage::TypeScript, + "views-query-builder-typescript", + TS_VIEWS_SUBSCRIBE_MODULE, + ) + .run() + .unwrap(); test.call("insert_player_proc", &["Alice"]).unwrap(); @@ -909,7 +960,10 @@ fn test_typescript_query_builder_view_query() { fn test_csharp_query_builder_view_query() { require_dotnet!(); let mut test = Smoketest::builder().autopublish(false).build(); - test.publish_csharp_module_source("views-csharp", "views-csharp", CS_VIEWS_QUERY_BUILDER_MODULE) + test.publish() + .name("views-csharp") + .source(ModuleLanguage::CSharp, "views-csharp", CS_VIEWS_QUERY_BUILDER_MODULE) + .run() .unwrap(); test.call("insert_value", &["0", "false"]).unwrap(); @@ -978,7 +1032,7 @@ fn run_pk_join_subscription_test(query: &str, projected_view_name: &str, mutatio // Switch to identity B so each underlying table has rows for 2 identities. test.new_identity().unwrap(); - let sub = test.subscribe_background(&[query], 4).unwrap(); + let sub = test.subscribe(&[query]).expect_rows(4).background().unwrap(); for mutation in mutations { apply_pk_join_mutation(&test, mutation); From 349c3e606db0d3c606b4c61f66bca6f90d9c2725 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 16:01:37 -0400 Subject: [PATCH 04/11] Address smoketest publish builder review --- crates/smoketests/src/lib.rs | 199 +++++++++++++++-------------------- 1 file changed, 87 insertions(+), 112 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 0bbf122f743..83dfe2c8b0f 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -517,16 +517,6 @@ struct ModuleSource { module_source: String, } -struct PublishCommand<'a> { - name: Option<&'a str>, - clear: bool, - break_clients: bool, - num_replicas: Option, - organization: Option<&'a str>, - force: bool, - stdin_input: Option<&'a str>, -} - impl<'a> PublishBuilder<'a> { pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); @@ -613,15 +603,93 @@ impl<'a> PublishBuilder<'a> { ); } - smoketest.publish_module_internal(PublishCommand { - name: name.as_deref(), - clear, - break_clients, - num_replicas, - organization: organization.as_deref(), - force, - stdin_input: stdin_input.as_deref(), - }) + let start = Instant::now(); + + let wasm_path_str = if let Some(ref precompiled_path) = smoketest.precompiled_wasm_path { + eprintln!("[TIMING] spacetime build: skipped (using precompiled)"); + precompiled_path.to_str().unwrap().to_string() + } else { + let project_path = smoketest.project_dir.path().to_str().unwrap().to_string(); + let build_start = Instant::now(); + let cli_path = ensure_binaries_built(); + let target_dir = shared_target_dir(); + + let mut build_cmd = Command::new(&cli_path); + build_cmd + .args(["build", "--module-path", &project_path]) + .current_dir(smoketest.project_dir.path()) + .env("CARGO_TARGET_DIR", &target_dir); + + let build_output = build_cmd.output().expect("Failed to execute spacetime build"); + eprintln!("[TIMING] spacetime build: {:?}", build_start.elapsed()); + + if !build_output.status.success() { + bail!( + "spacetime build failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&build_output.stdout), + String::from_utf8_lossy(&build_output.stderr) + ); + } + + let wasm_filename = format!("{}.wasm", smoketest.module_name); + let wasm_path = target_dir.join("wasm32-unknown-unknown/release").join(&wasm_filename); + wasm_path.to_str().unwrap().to_string() + }; + + let publish_start = Instant::now(); + let mut args = vec![ + "publish", + "--server", + &smoketest.server_url, + "--bin-path", + &wasm_path_str, + ]; + + if force { + args.push("--yes"); + } + + if clear { + args.push("--clear-database"); + } + + if break_clients { + args.push("--break-clients"); + } + + let num_replicas_owned = num_replicas.map(|n| n.to_string()); + if let Some(n) = num_replicas_owned.as_ref() { + args.push("--num-replicas"); + args.push(n); + } + + if let Some(org) = organization.as_deref() { + args.push("--organization"); + args.push(org); + } + + if let Some(n) = name.as_deref() { + args.push(n); + } + + let output = match stdin_input.as_deref() { + Some(stdin_input) => smoketest.spacetime_with_stdin(&args, stdin_input)?, + None => smoketest.spacetime(&args)?, + }; + eprintln!( + "[TIMING] spacetime publish (after build): {:?}", + publish_start.elapsed() + ); + eprintln!("[TIMING] publish_module total: {:?}", start.elapsed()); + + let re = Regex::new(r"identity: ([0-9a-fA-F]+)").unwrap(); + if let Some(caps) = re.captures(&output) { + let identity = caps.get(1).unwrap().as_str().to_string(); + smoketest.database_identity = Some(identity.clone()); + Ok(identity) + } else { + bail!("Failed to parse database identity from publish output: {}", output); + } } } @@ -1353,99 +1421,6 @@ log = "0.4" } } - fn publish_module_internal(&mut self, publish: PublishCommand<'_>) -> Result { - let start = Instant::now(); - - // Determine the WASM path - either precompiled or build it - let wasm_path_str = if let Some(ref precompiled_path) = self.precompiled_wasm_path { - // Use pre-compiled WASM directly (no build needed) - eprintln!("[TIMING] spacetime build: skipped (using precompiled)"); - precompiled_path.to_str().unwrap().to_string() - } else { - // Build the WASM module from source - let project_path = self.project_dir.path().to_str().unwrap().to_string(); - let build_start = Instant::now(); - let cli_path = ensure_binaries_built(); - let target_dir = shared_target_dir(); - - let mut build_cmd = Command::new(&cli_path); - build_cmd - .args(["build", "--module-path", &project_path]) - .current_dir(self.project_dir.path()) - .env("CARGO_TARGET_DIR", &target_dir); - - let build_output = build_cmd.output().expect("Failed to execute spacetime build"); - let build_elapsed = build_start.elapsed(); - eprintln!("[TIMING] spacetime build: {:?}", build_elapsed); - - if !build_output.status.success() { - bail!( - "spacetime build failed:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&build_output.stdout), - String::from_utf8_lossy(&build_output.stderr) - ); - } - - // Construct the wasm path using the unique module name - let wasm_filename = format!("{}.wasm", self.module_name); - let wasm_path = target_dir.join("wasm32-unknown-unknown/release").join(&wasm_filename); - wasm_path.to_str().unwrap().to_string() - }; - - // Now publish with --bin-path to skip rebuild - let publish_start = Instant::now(); - let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; - - if publish.force { - args.push("--yes"); - } - - if publish.clear { - args.push("--clear-database"); - } - - if publish.break_clients { - args.push("--break-clients"); - } - - let num_replicas_owned = publish.num_replicas.map(|n| n.to_string()); - if let Some(n) = num_replicas_owned.as_ref() { - args.push("--num-replicas"); - args.push(n); - } - - if let Some(org) = publish.organization { - args.push("--organization"); - args.push(org); - } - - let name_owned; - if let Some(n) = publish.name { - name_owned = n.to_string(); - args.push(&name_owned); - } - - let output = match publish.stdin_input { - Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?, - None => self.spacetime(&args)?, - }; - eprintln!( - "[TIMING] spacetime publish (after build): {:?}", - publish_start.elapsed() - ); - eprintln!("[TIMING] publish_module total: {:?}", start.elapsed()); - - // Parse the identity from output like "identity: abc123..." - let re = Regex::new(r"identity: ([0-9a-fA-F]+)").unwrap(); - if let Some(caps) = re.captures(&output) { - let identity = caps.get(1).unwrap().as_str().to_string(); - self.database_identity = Some(identity.clone()); - Ok(identity) - } else { - bail!("Failed to parse database identity from publish output: {}", output); - } - } - /// Calls a reducer or procedure with the given arguments. /// /// Arguments are passed directly to the CLI as strings. From b83b803ec60fc1c0c91518f6b9969b7d56c43262 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 16:51:42 -0400 Subject: [PATCH 05/11] Finish publish builder review fixes --- crates/smoketests/src/lib.rs | 233 ++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 115 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 83dfe2c8b0f..9e390d60363 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -594,102 +594,37 @@ impl<'a> PublishBuilder<'a> { if let Some(source) = source { let module_name = name.as_deref().context("No module name provided for source publish")?; - return smoketest.publish_module_source_internal( - source.language, - &source.project_dir_name, - module_name, - &source.module_source, - clear, - ); - } - - let start = Instant::now(); - - let wasm_path_str = if let Some(ref precompiled_path) = smoketest.precompiled_wasm_path { - eprintln!("[TIMING] spacetime build: skipped (using precompiled)"); - precompiled_path.to_str().unwrap().to_string() - } else { - let project_path = smoketest.project_dir.path().to_str().unwrap().to_string(); - let build_start = Instant::now(); - let cli_path = ensure_binaries_built(); - let target_dir = shared_target_dir(); - - let mut build_cmd = Command::new(&cli_path); - build_cmd - .args(["build", "--module-path", &project_path]) - .current_dir(smoketest.project_dir.path()) - .env("CARGO_TARGET_DIR", &target_dir); - - let build_output = build_cmd.output().expect("Failed to execute spacetime build"); - eprintln!("[TIMING] spacetime build: {:?}", build_start.elapsed()); - - if !build_output.status.success() { - bail!( - "spacetime build failed:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&build_output.stdout), - String::from_utf8_lossy(&build_output.stderr) - ); - } - - let wasm_filename = format!("{}.wasm", smoketest.module_name); - let wasm_path = target_dir.join("wasm32-unknown-unknown/release").join(&wasm_filename); - wasm_path.to_str().unwrap().to_string() - }; - - let publish_start = Instant::now(); - let mut args = vec![ - "publish", - "--server", - &smoketest.server_url, - "--bin-path", - &wasm_path_str, - ]; - - if force { - args.push("--yes"); + return match source.language { + ModuleLanguage::TypeScript => smoketest.publish_typescript_module_source_internal( + &source.project_dir_name, + module_name, + &source.module_source, + clear, + ), + ModuleLanguage::CSharp => smoketest.publish_csharp_module_source_internal( + &source.project_dir_name, + module_name, + &source.module_source, + clear, + ), + ModuleLanguage::Cpp => smoketest.publish_cpp_module_source_internal( + &source.project_dir_name, + module_name, + &source.module_source, + clear, + ), + }; } - if clear { - args.push("--clear-database"); - } - - if break_clients { - args.push("--break-clients"); - } - - let num_replicas_owned = num_replicas.map(|n| n.to_string()); - if let Some(n) = num_replicas_owned.as_ref() { - args.push("--num-replicas"); - args.push(n); - } - - if let Some(org) = organization.as_deref() { - args.push("--organization"); - args.push(org); - } - - if let Some(n) = name.as_deref() { - args.push(n); - } - - let output = match stdin_input.as_deref() { - Some(stdin_input) => smoketest.spacetime_with_stdin(&args, stdin_input)?, - None => smoketest.spacetime(&args)?, - }; - eprintln!( - "[TIMING] spacetime publish (after build): {:?}", - publish_start.elapsed() - ); - eprintln!("[TIMING] publish_module total: {:?}", start.elapsed()); - - let re = Regex::new(r"identity: ([0-9a-fA-F]+)").unwrap(); - if let Some(caps) = re.captures(&output) { - let identity = caps.get(1).unwrap().as_str().to_string(); - smoketest.database_identity = Some(identity.clone()); - Ok(identity) - } else { - bail!("Failed to parse database identity from publish output: {}", output); - } + smoketest.publish_module_internal( + name.as_deref(), + clear, + break_clients, + num_replicas, + organization.as_deref(), + force, + stdin_input.as_deref(), + ) } } @@ -1144,27 +1079,6 @@ impl Smoketest { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } - fn publish_module_source_internal( - &mut self, - language: ModuleLanguage, - project_dir_name: &str, - module_name: &str, - module_source: &str, - clear: bool, - ) -> Result { - match language { - ModuleLanguage::TypeScript => { - self.publish_typescript_module_source_internal(project_dir_name, module_name, module_source, clear) - } - ModuleLanguage::CSharp => { - self.publish_csharp_module_source_internal(project_dir_name, module_name, module_source, clear) - } - ModuleLanguage::Cpp => { - self.publish_cpp_module_source_internal(project_dir_name, module_name, module_source, clear) - } - } - } - fn publish_typescript_module_source_internal( &mut self, project_dir_name: &str, @@ -1421,6 +1335,95 @@ log = "0.4" } } + #[allow(clippy::too_many_arguments)] + fn publish_module_internal( + &mut self, + name: Option<&str>, + clear: bool, + break_clients: bool, + num_replicas: Option, + organization: Option<&str>, + force: bool, + stdin_input: Option<&str>, + ) -> Result { + let start = Instant::now(); + + let wasm_path_str = if let Some(ref precompiled_path) = self.precompiled_wasm_path { + eprintln!("[TIMING] spacetime build: skipped (using precompiled)"); + precompiled_path.to_str().unwrap().to_string() + } else { + let project_path = self.project_dir.path().to_str().unwrap().to_string(); + let build_start = Instant::now(); + let cli_path = ensure_binaries_built(); + let target_dir = shared_target_dir(); + + let mut build_cmd = Command::new(&cli_path); + build_cmd + .args(["build", "--module-path", &project_path]) + .current_dir(self.project_dir.path()) + .env("CARGO_TARGET_DIR", &target_dir); + + let build_output = build_cmd.output().expect("Failed to execute spacetime build"); + eprintln!("[TIMING] spacetime build: {:?}", build_start.elapsed()); + + if !build_output.status.success() { + bail!( + "spacetime build failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&build_output.stdout), + String::from_utf8_lossy(&build_output.stderr) + ); + } + + let wasm_filename = format!("{}.wasm", self.module_name); + let wasm_path = target_dir.join("wasm32-unknown-unknown/release").join(&wasm_filename); + wasm_path.to_str().unwrap().to_string() + }; + + let publish_start = Instant::now(); + let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; + + if force { + args.push("--yes"); + } + + if clear { + args.push("--clear-database"); + } + + if break_clients { + args.push("--break-clients"); + } + + let num_replicas_owned = num_replicas.map(|n| n.to_string()); + if let Some(n) = num_replicas_owned.as_ref() { + args.push("--num-replicas"); + args.push(n); + } + + if let Some(org) = organization { + args.push("--organization"); + args.push(org); + } + + if let Some(n) = name { + args.push(n); + } + + let output = match stdin_input { + Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?, + None => self.spacetime(&args)?, + }; + eprintln!( + "[TIMING] spacetime publish (after build): {:?}", + publish_start.elapsed() + ); + eprintln!("[TIMING] publish_module total: {:?}", start.elapsed()); + + parse_identity_from_publish_output(&output).inspect(|identity| { + self.database_identity = Some(identity.clone()); + }) + } + /// Calls a reducer or procedure with the given arguments. /// /// Arguments are passed directly to the CLI as strings. From 350de43de6813e2454e6b75b2c841c2d23339995 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 17:40:08 -0400 Subject: [PATCH 06/11] Restore smoketest builder docs --- crates/smoketests/src/lib.rs | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 9e390d60363..38a35b0fcfc 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -518,42 +518,58 @@ struct ModuleSource { } impl<'a> PublishBuilder<'a> { + /// Publishes the module with a specific database name. pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } + /// If `clear` is true, the database will be cleared before publishing. pub fn clear(mut self, clear: bool) -> Self { self.clear = clear; self } + /// Publishes with the `--break-clients` flag. pub fn break_clients(mut self, break_clients: bool) -> Self { self.break_clients = break_clients; self } + /// Publishes with a specific replica count. pub fn num_replicas(mut self, num_replicas: u32) -> Self { self.num_replicas = Some(num_replicas); self } + /// Publishes into a specific organization. pub fn organization(mut self, organization: impl Into) -> Self { self.organization = Some(organization.into()); self } + /// Controls whether publish passes `--yes`. + /// + /// Set to false when testing interactive publish prompts. pub fn force(mut self, force: bool) -> Self { self.force = force; self } + /// Publishes the module and supplies stdin input to the CLI. + /// + /// Useful for interactive publish prompts which require typed acknowledgements. + /// This does not pass `--yes` so that interactive prompts are not suppressed. pub fn stdin(mut self, stdin_input: impl Into) -> Self { self.force = false; self.stdin_input = Some(stdin_input.into()); self } + /// Re-publishes the module to the existing database identity. + /// + /// This is useful for testing auto-migrations where you want to update + /// the module without clearing the database. pub fn current_database(mut self) -> Result { let identity = self .smoketest @@ -565,6 +581,10 @@ impl<'a> PublishBuilder<'a> { Ok(self) } + /// Initializes, writes, and publishes a module from source. + /// + /// The module is created under `/`. + /// On success this updates `self.database_identity`. pub fn source( mut self, language: ModuleLanguage, @@ -579,6 +599,7 @@ impl<'a> PublishBuilder<'a> { self } + /// Publishes the module and stores the database identity. pub fn run(self) -> Result { let PublishBuilder { smoketest, @@ -637,21 +658,28 @@ pub struct SubscribeBuilder<'a> { } impl<'a> SubscribeBuilder<'a> { + /// Starts the subscription against a specific database. pub fn database(mut self, database: impl Into) -> Self { self.database = Some(database.into()); self } + /// Waits for this many updates before returning. pub fn expect_rows(mut self, expected_rows: usize) -> Self { self.expected_rows = expected_rows; self } + /// Sets the `--confirmed` flag for the subscription. pub fn confirmed(mut self, confirmed: bool) -> Self { self.confirmed = Some(confirmed); self } + /// Starts a subscription and waits for the expected number of updates. + /// + /// Returns the updates as JSON values. For tests that need to perform + /// actions while subscribed, use `background` instead. pub fn run(self) -> Result> { let start = Instant::now(); let owned_identity; @@ -671,6 +699,10 @@ impl<'a> SubscribeBuilder<'a> { .subscribe_on_impl(database, &queries, self.expected_rows, self.confirmed, start) } + /// Starts a subscription in the background and returns a handle. + /// + /// This matches Python's subscribe semantics: start subscription first, + /// perform actions, then call the handle to collect results. pub fn background(self) -> Result { let owned_identity; let database = if let Some(database) = self.database.as_deref() { @@ -1321,6 +1353,9 @@ log = "0.4" output } + /// Creates a builder for publishing the module. + /// + /// By default, publish stores the database identity and passes `--yes`. pub fn publish(&mut self) -> PublishBuilder<'_> { PublishBuilder { smoketest: self, @@ -1677,6 +1712,10 @@ log = "0.4" Ok(ApiResponse { status_code, body }) } + /// Creates a builder for starting a subscription. + /// + /// By default, this subscribes to the current database, waits for one update, + /// and leaves the CLI `--confirmed` flag unset. pub fn subscribe(&self, queries: &[&str]) -> SubscribeBuilder<'_> { SubscribeBuilder { smoketest: self, From cf002742c554d6b31cf1a3ed04d03fea4746a6d7 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 17:44:37 -0400 Subject: [PATCH 07/11] Revert "Restore smoketest builder docs" This reverts commit 350de43de6813e2454e6b75b2c841c2d23339995. --- crates/smoketests/src/lib.rs | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 38a35b0fcfc..9e390d60363 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -518,58 +518,42 @@ struct ModuleSource { } impl<'a> PublishBuilder<'a> { - /// Publishes the module with a specific database name. pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } - /// If `clear` is true, the database will be cleared before publishing. pub fn clear(mut self, clear: bool) -> Self { self.clear = clear; self } - /// Publishes with the `--break-clients` flag. pub fn break_clients(mut self, break_clients: bool) -> Self { self.break_clients = break_clients; self } - /// Publishes with a specific replica count. pub fn num_replicas(mut self, num_replicas: u32) -> Self { self.num_replicas = Some(num_replicas); self } - /// Publishes into a specific organization. pub fn organization(mut self, organization: impl Into) -> Self { self.organization = Some(organization.into()); self } - /// Controls whether publish passes `--yes`. - /// - /// Set to false when testing interactive publish prompts. pub fn force(mut self, force: bool) -> Self { self.force = force; self } - /// Publishes the module and supplies stdin input to the CLI. - /// - /// Useful for interactive publish prompts which require typed acknowledgements. - /// This does not pass `--yes` so that interactive prompts are not suppressed. pub fn stdin(mut self, stdin_input: impl Into) -> Self { self.force = false; self.stdin_input = Some(stdin_input.into()); self } - /// Re-publishes the module to the existing database identity. - /// - /// This is useful for testing auto-migrations where you want to update - /// the module without clearing the database. pub fn current_database(mut self) -> Result { let identity = self .smoketest @@ -581,10 +565,6 @@ impl<'a> PublishBuilder<'a> { Ok(self) } - /// Initializes, writes, and publishes a module from source. - /// - /// The module is created under `/`. - /// On success this updates `self.database_identity`. pub fn source( mut self, language: ModuleLanguage, @@ -599,7 +579,6 @@ impl<'a> PublishBuilder<'a> { self } - /// Publishes the module and stores the database identity. pub fn run(self) -> Result { let PublishBuilder { smoketest, @@ -658,28 +637,21 @@ pub struct SubscribeBuilder<'a> { } impl<'a> SubscribeBuilder<'a> { - /// Starts the subscription against a specific database. pub fn database(mut self, database: impl Into) -> Self { self.database = Some(database.into()); self } - /// Waits for this many updates before returning. pub fn expect_rows(mut self, expected_rows: usize) -> Self { self.expected_rows = expected_rows; self } - /// Sets the `--confirmed` flag for the subscription. pub fn confirmed(mut self, confirmed: bool) -> Self { self.confirmed = Some(confirmed); self } - /// Starts a subscription and waits for the expected number of updates. - /// - /// Returns the updates as JSON values. For tests that need to perform - /// actions while subscribed, use `background` instead. pub fn run(self) -> Result> { let start = Instant::now(); let owned_identity; @@ -699,10 +671,6 @@ impl<'a> SubscribeBuilder<'a> { .subscribe_on_impl(database, &queries, self.expected_rows, self.confirmed, start) } - /// Starts a subscription in the background and returns a handle. - /// - /// This matches Python's subscribe semantics: start subscription first, - /// perform actions, then call the handle to collect results. pub fn background(self) -> Result { let owned_identity; let database = if let Some(database) = self.database.as_deref() { @@ -1353,9 +1321,6 @@ log = "0.4" output } - /// Creates a builder for publishing the module. - /// - /// By default, publish stores the database identity and passes `--yes`. pub fn publish(&mut self) -> PublishBuilder<'_> { PublishBuilder { smoketest: self, @@ -1712,10 +1677,6 @@ log = "0.4" Ok(ApiResponse { status_code, body }) } - /// Creates a builder for starting a subscription. - /// - /// By default, this subscribes to the current database, waits for one update, - /// and leaves the CLI `--confirmed` flag unset. pub fn subscribe(&self, queries: &[&str]) -> SubscribeBuilder<'_> { SubscribeBuilder { smoketest: self, From 1fe5d67835f7e134f2dd284cee61d331069e1f1a Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Thu, 11 Jun 2026 17:45:41 -0400 Subject: [PATCH 08/11] Address smoketest builder review --- crates/smoketests/src/lib.rs | 53 ++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 9e390d60363..f39dee6af12 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -518,6 +518,20 @@ struct ModuleSource { } impl<'a> PublishBuilder<'a> { + fn new(smoketest: &'a mut Smoketest) -> Self { + Self { + smoketest, + name: None, + clear: false, + break_clients: false, + num_replicas: None, + organization: None, + force: true, + stdin_input: None, + source: None, + } + } + pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self @@ -637,6 +651,16 @@ pub struct SubscribeBuilder<'a> { } impl<'a> SubscribeBuilder<'a> { + fn new(smoketest: &'a Smoketest, queries: &[&str]) -> Self { + Self { + smoketest, + database: None, + queries: queries.iter().map(|query| query.to_string()).collect(), + expected_rows: 1, + confirmed: None, + } + } + pub fn database(mut self, database: impl Into) -> Self { self.database = Some(database.into()); self @@ -1322,19 +1346,18 @@ log = "0.4" } pub fn publish(&mut self) -> PublishBuilder<'_> { - PublishBuilder { - smoketest: self, - name: None, - clear: false, - break_clients: false, - num_replicas: None, - organization: None, - force: true, - stdin_input: None, - source: None, - } + PublishBuilder::new(self) } + /// Publishes the module and stores the database identity. + /// + /// If `name` is provided, the database will be published with that name. + /// If `clear` is true, the database will be cleared before publishing. + /// If `force` is false, the publish command will not pass `--yes`, so interactive prompts are not suppressed. + /// If `stdin_input` is provided, it will be passed to the CLI for interactive prompts. + /// + /// When `name` is an existing database identity, this re-publishes to that database, which is useful for testing + /// auto-migrations where you want to update the module without clearing the database. #[allow(clippy::too_many_arguments)] fn publish_module_internal( &mut self, @@ -1678,13 +1701,7 @@ log = "0.4" } pub fn subscribe(&self, queries: &[&str]) -> SubscribeBuilder<'_> { - SubscribeBuilder { - smoketest: self, - database: None, - queries: queries.iter().map(|query| query.to_string()).collect(), - expected_rows: 1, - confirmed: None, - } + SubscribeBuilder::new(self, queries) } fn subscribe_on_impl( From 3afc8ba7fcfa667079bcc4c82552d5b0bbf66c3a Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Fri, 12 Jun 2026 01:59:51 -0400 Subject: [PATCH 09/11] Restore smoketest publish comments --- crates/smoketests/src/lib.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index c240eb27f37..cc55510e528 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -656,7 +656,7 @@ impl<'a> SubscribeBuilder<'a> { smoketest, database: None, queries: queries.iter().map(|query| query.to_string()).collect(), - expected_rows: 1, + expected_rows: Some(1), confirmed: None, } } @@ -667,7 +667,7 @@ impl<'a> SubscribeBuilder<'a> { } pub fn expect_rows(mut self, expected_rows: usize) -> Self { - self.expected_rows = expected_rows; + self.expected_rows = Some(expected_rows); self } @@ -1371,10 +1371,13 @@ log = "0.4" ) -> Result { let start = Instant::now(); + // Determine the WASM path - either precompiled or build it let wasm_path_str = if let Some(ref precompiled_path) = self.precompiled_wasm_path { + // Use pre-compiled WASM directly (no build needed) eprintln!("[TIMING] spacetime build: skipped (using precompiled)"); precompiled_path.to_str().unwrap().to_string() } else { + // Build the WASM module from source let project_path = self.project_dir.path().to_str().unwrap().to_string(); let build_start = Instant::now(); let cli_path = ensure_binaries_built(); @@ -1397,11 +1400,13 @@ log = "0.4" ); } + // Construct the wasm path using the unique module name let wasm_filename = format!("{}.wasm", self.module_name); let wasm_path = target_dir.join("wasm32-unknown-unknown/release").join(&wasm_filename); wasm_path.to_str().unwrap().to_string() }; + // Now publish with --bin-path to skip rebuild let publish_start = Instant::now(); let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; @@ -1743,7 +1748,7 @@ log = "0.4" .stderr(Stdio::piped()); let output = cmd.output().context("Failed to run subscribe command")?; - eprintln!("[TIMING] subscribe (n={}): {:?}", n, start.elapsed()); + eprintln!("[TIMING] subscribe (n={:?}): {:?}", n, start.elapsed()); if !output.status.success() { bail!("subscribe failed:\nstderr: {}", String::from_utf8_lossy(&output.stderr)); From d67ecf82ad75ca28aef94b2aca73f7a81000ce3d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Fri, 12 Jun 2026 08:52:33 -0700 Subject: [PATCH 10/11] [bot/smoketest-builder-options]: fix bad merge --- crates/smoketests/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index cc55510e528..ad06767c2dc 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -656,7 +656,7 @@ impl<'a> SubscribeBuilder<'a> { smoketest, database: None, queries: queries.iter().map(|query| query.to_string()).collect(), - expected_rows: Some(1), + expected_rows: None, confirmed: None, } } From 4810350ac36477cc1d09f9ddbd0f0d4165637328 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Fri, 12 Jun 2026 12:19:00 -0400 Subject: [PATCH 11/11] Fix smoketest builder merge fallout --- crates/smoketests/tests/smoketests/auto_migration.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/smoketests/tests/smoketests/auto_migration.rs b/crates/smoketests/tests/smoketests/auto_migration.rs index 781d1a1478a..70f84b394b6 100644 --- a/crates/smoketests/tests/smoketests/auto_migration.rs +++ b/crates/smoketests/tests/smoketests/auto_migration.rs @@ -621,7 +621,10 @@ fn automigrate_drop_event_table_replays_after_restart() { // Drop the event table. test.write_module_code(MODULE_CODE_DROP_EVENT_TABLE_AFTER).unwrap(); - test.publish_module_with_options(&identity, false, true) + test.publish() + .name(&identity) + .break_clients(true) + .run() .expect("Dropping the event table should succeed"); // Wait until data written after the drop is durable,