Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dde70f6
[bfops/rust-smoketests-lib]: more things
bfops Feb 11, 2026
b44ad02
[bfops/rust-smoketests-lib]: debug changes
bfops Feb 16, 2026
2cda6c7
[bfops/rust-smoketests-lib]: Merge remote-tracking branch 'origin/mas…
bfops Mar 9, 2026
033fc8f
[bfops/rust-smoketests-lib]: commit test change
bfops Mar 9, 2026
40e5eba
[bfops/rust-smoketests-lib]: revert
bfops Mar 9, 2026
5186ddb
[bfops/rust-smoketests-lib]: revert
bfops Mar 9, 2026
c1dfed1
Merge branch 'master' into bfops/rust-smoketests-lib
bfops Mar 19, 2026
684627b
[bfops/rust-smoketests-lib]: updates
bfops Mar 19, 2026
8cb4a3a
Merge remote-tracking branch 'origin/master' into bfops/rust-smoketes…
bfops Apr 10, 2026
f143b04
WIP revert this commit
bfops Apr 13, 2026
a40c919
fix renames
bfops Apr 13, 2026
378fceb
Revert "WIP revert this commit"
bfops Apr 13, 2026
ef673e6
revert
bfops Apr 13, 2026
078383d
fix lints
bfops Apr 13, 2026
f9f5e77
fix renames again
bfops Apr 13, 2026
72377c8
http tests require local
bfops Apr 13, 2026
a0e5554
Apply suggestion from @bfops
bfops Apr 13, 2026
01743e7
simplify
bfops Apr 13, 2026
9d5e6ac
Merge branch 'bfops/rust-smoketests-lib' of github.com:clockworklabs/…
bfops Apr 13, 2026
4058cad
simplify
bfops Apr 13, 2026
d2c74d5
simplify
bfops Apr 13, 2026
1d30f5e
lint and fix
bfops Apr 13, 2026
8da3b0c
temp build lob
bfops Apr 13, 2026
1c29047
revert this
bfops Apr 13, 2026
85b3f9d
revert this
bfops Apr 13, 2026
5c4a04f
stderr as well
bfops Apr 13, 2026
e9b02aa
Merge branch 'master' into bfops/rust-smoketests-lib
bfops Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ concurrency:

jobs:
smoketests:
needs: [lints]
name: Smoketests (${{ matrix.name }})
strategy:
matrix:
Expand Down Expand Up @@ -159,7 +158,7 @@ jobs:
if [ -f ~/emsdk/emsdk_env.sh ]; then
source ~/emsdk/emsdk_env.sh
fi
cargo ci smoketests -- --test-threads=1
cargo ci smoketests -- --test-threads=1 |& tee $RUNNER_TEMP/build.log

# Due to Emscripten PATH issues this was separated to make sure OpenSSL still builds correctly
- name: Run smoketests (Windows)
Expand All @@ -169,7 +168,7 @@ jobs:
if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") {
& "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null
}
cargo ci smoketests -- --test-threads=1
cargo ci smoketests -- --test-threads=1

- name: Check for changes
run: |
Expand Down
198 changes: 171 additions & 27 deletions crates/smoketests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use regex::Regex;
use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard};
use std::env;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::OnceLock;
Expand Down Expand Up @@ -401,6 +402,29 @@ impl ApiResponse {
}
}

#[derive(Clone, Debug)]
pub struct PublishOptions {
pub clear: bool,
pub break_clients: bool,
pub num_replicas: Option<u32>,
pub organization: Option<String>,
pub force: bool,
pub stdin_input: Option<String>,
}

impl Default for PublishOptions {
fn default() -> Self {
Self {
clear: false,
break_clients: false,
num_replicas: None,
organization: None,
force: true,
stdin_input: None,
}
}
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

started refactoring to the builder pattern so we stop having a billion different versions of the publish function.

I would like to do this for several other parts of this lib as well, but it's not an imminent priority.


/// Builder for creating `Smoketest` instances.
pub struct SmoketestBuilder {
module_code: Option<String>,
Expand All @@ -409,6 +433,7 @@ pub struct SmoketestBuilder {
extra_deps: String,
autopublish: bool,
pg_port: Option<u16>,
server_url_override: Option<String>,
}

impl Default for SmoketestBuilder {
Expand All @@ -427,9 +452,15 @@ impl SmoketestBuilder {
extra_deps: String::new(),
autopublish: true,
pg_port: None,
server_url_override: None,
}
}

pub fn server_url(mut self, url: &str) -> Self {
self.server_url_override = Some(url.to_string());
self
}

/// Enables the PostgreSQL wire protocol on the specified port.
pub fn pg_port(mut self, port: u16) -> Self {
self.pg_port = Some(port);
Expand Down Expand Up @@ -503,7 +534,10 @@ impl SmoketestBuilder {
let build_start = Instant::now();

// Check if we're running against a remote server
let (guard, server_url) = if let Some(remote_url) = remote_server_url() {
let (guard, server_url) = if let Some(url) = self.server_url_override {
eprintln!("[REMOTE] Using explicit server URL: {}", url);
(None, url)
} else if let Some(remote_url) = remote_server_url() {
eprintln!("[REMOTE] Using remote server: {}", remote_url);
(None, remote_url)
} else {
Expand Down Expand Up @@ -629,6 +663,16 @@ impl Smoketest {
.context("No spacetimedb_token found in config")
}

pub fn login_with_token(&self, token: &str) -> Result<()> {
let host = self.server_host();
let config_str = format!(
"default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"http\"\n",
token, host
);
fs::write(&self.config_path, config_str).context("Failed to write config.toml")?;
Ok(())
}

/// Runs psql command against the PostgreSQL wire protocol server.
///
/// Returns the output on success, or an error with stderr on failure.
Expand Down Expand Up @@ -998,15 +1042,25 @@ log = "0.4"

/// Publishes the module and stores the database identity.
pub fn publish_module(&mut self) -> Result<String> {
self.publish_module_opts(None, false)
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<String> {
self.publish_module_opts(Some(name), clear)
self.publish_module_internal_ext(
Some(name),
PublishOptions {
clear,
..PublishOptions::default()
},
)
}

pub fn publish_module_named_ext(&mut self, name: &str, opts: PublishOptions) -> Result<String> {
self.publish_module_internal_ext(Some(name), opts)
}

/// Re-publishes the module to the existing database identity with optional clear.
Expand All @@ -1019,41 +1073,58 @@ log = "0.4"
.as_ref()
.context("No database published yet")?
.clone();
self.publish_module_opts(Some(&identity), clear)
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<String> {
self.publish_module_internal(Some(name), clear, break_clients, true, None)
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<String> {
self.publish_module_internal(Some(name), false, false, false, Some(stdin_input))
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<String> {
self.publish_module_internal(Some(name), false, false, false, None)
self.publish_module_internal_ext(
Some(name),
PublishOptions {
force: false,
..PublishOptions::default()
},
)
}

/// Internal helper for publishing with options.
fn publish_module_opts(&mut self, name: Option<&str>, clear: bool) -> Result<String> {
self.publish_module_internal(name, clear, false, true, None)
pub fn publish_module_with_options_ext(&mut self, name: &str, opts: PublishOptions) -> Result<String> {
self.publish_module_internal_ext(Some(name), opts)
}

/// Internal helper for publishing with all options.
fn publish_module_internal(
&mut self,
name: Option<&str>,
clear: bool,
break_clients: bool,
force: bool,
stdin_input: Option<&str>,
) -> Result<String> {
fn publish_module_internal_ext(&mut self, name: Option<&str>, opts: PublishOptions) -> Result<String> {
let start = Instant::now();

// Determine the WASM path - either precompiled or build it
Expand Down Expand Up @@ -1096,25 +1167,37 @@ 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 opts.force {
args.push("--yes");
}

if clear {
if opts.clear {
args.push("--clear-database");
}

if break_clients {
if opts.break_clients {
args.push("--break-clients");
}

let num_replicas_owned = opts.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() {
args.push("--organization");
args.push(org);
}

let name_owned;
if let Some(n) = name {
name_owned = n.to_string();
args.push(&name_owned);
}

let output = match stdin_input {
let output = match opts.stdin_input.as_deref() {
Some(stdin_input) => self.spacetime_with_stdin(&args, stdin_input)?,
None => self.spacetime(&args)?,
};
Expand Down Expand Up @@ -1396,15 +1479,45 @@ log = "0.4"
self.subscribe_opts(queries, n, None)
}

pub fn subscribe_on(&self, database: &str, queries: &[&str], n: usize) -> Result<Vec<serde_json::Value>> {
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<Vec<serde_json::Value>> {
self.subscribe_opts(queries, n, Some(true))
}

pub fn subscribe_on_confirmed(&self, database: &str, queries: &[&str], n: usize) -> Result<Vec<serde_json::Value>> {
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<bool>) -> Result<Vec<serde_json::Value>> {
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<bool>,
) -> Result<Vec<serde_json::Value>> {
let start = Instant::now();
self.subscribe_on_impl(database, queries, n, confirmed, start)
}

fn subscribe_on_impl(
&self,
database: &str,
queries: &[&str],
n: usize,
confirmed: Option<bool>,
start: Instant,
) -> Result<Vec<serde_json::Value>> {
let config_path_str = self.config_path.to_str().unwrap();

let cli_path = ensure_binaries_built();
Expand All @@ -1415,7 +1528,7 @@ log = "0.4"
"subscribe".to_string(),
"--server".to_string(),
self.server_url.to_string(),
identity.to_string(),
database.to_string(),
"-t".to_string(),
"30".to_string(),
"-n".to_string(),
Expand Down Expand Up @@ -1456,6 +1569,10 @@ log = "0.4"
self.subscribe_background_opts(queries, n, None)
}

pub fn subscribe_background_on(&self, database: &str, queries: &[&str], n: usize) -> Result<SubscriptionHandle> {
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<SubscriptionHandle> {
self.subscribe_background_opts(queries, n, Some(true))
Expand All @@ -1466,21 +1583,48 @@ log = "0.4"
self.subscribe_background_opts(queries, n, Some(false))
}

pub fn subscribe_background_on_confirmed(
&self,
database: &str,
queries: &[&str],
n: usize,
) -> Result<SubscriptionHandle> {
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<bool>,
) -> Result<SubscriptionHandle> {
use std::io::{BufRead, BufReader};

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<bool>,
) -> Result<SubscriptionHandle> {
self.subscribe_background_on_impl(database, queries, n, confirmed)
}

fn subscribe_background_on_impl(
&self,
database: &str,
queries: &[&str],
n: usize,
confirmed: Option<bool>,
) -> Result<SubscriptionHandle> {
let cli_path = ensure_binaries_built();
let mut cmd = Command::new(&cli_path);
// Use --print-initial-update so we know when subscription is established
Expand All @@ -1491,7 +1635,7 @@ log = "0.4"
"subscribe".to_string(),
"--server".to_string(),
self.server_url.clone(),
identity,
database.to_string(),
"-t".to_string(),
"30".to_string(),
"-n".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion crates/smoketests/tests/smoketests/add_remove_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ fn test_add_then_remove_index() {
.build();

// TODO: Does the name do anything? Other tests just let the DB assign.
let name = format!("test-db-{}", std::process::id());
let name = format!("add-remove-index-{}", std::process::id());

// Publish and attempt a subscribing to a join query.
// There are no indices, resulting in an unsupported unindexed join.
Expand Down
Loading
Loading