Translate smoketests from Python to Rust (#4102)

# Description of Changes

This PR translates all of our Python smoketests into Rust tests which
can be run from `cargo run`

## Motivation

The purpose of this fivefold:

1. All developers on the team are familiar with Rust
2. It simplifies our devops because we can drop Python as a dependency
to run the tests
3. You can now run all tests in the repo through the single `cargo test`
interface
4. Because we use the `SpacetimeDbGuard` and `cargo test`/`cargo
nextest` we can easily parallelize the smoke tests
5. The smoketests can now use machinery imported from SpacetimeDB crates
(e.g. `bsatn` etc.)

IMPORTANT NOTE!

There are several ways to implement the smoke tests in Rust (none are
great):

1. A separate xtask specifically for the smoke tests
- This doesn't solve the problem of the CLI tests which also use the
`guard` crate
    - Idiosyncratic way to run the smoke tests as opposed to cargo test
- Does NOT resolve the cargo within cargo problem because we still have
to build the test modules with cargo
2. A `build.rs` script in `guard` which first builds the executables as
a compile step for compiling guard
- Deadlocks on a cargo lock file conflict (Outer cargo compiles guard →
runs build.rs, inner cargo tries to acquire the build directory lock,
outer cargo holds the directory lock, deadlock)
- If you fix the deadlock by using different target dirs, it still looks
stuck on building guard because it's actually compiling all of
spacetimedb-standalone and spacetimedb-cli.
    - Still technically runs cargo inside of cargo.
3. Add `spacetimedb-cli` and `spacetimedb-standalone` as an artifact
dependency of the guard crate
- Has good and clear output but requires +nightly when running the
smoketests and CLI tests, otherwise won't do the right thing. See
https://github.com/rust-lang/cargo/issues/9096
4. Compile the executables at runtime during the tests themselves where
the first test takes a lock while the executables are building using
cargo within cargo
- Makes the tests look like they're taking a long time when they're just
waiting for the build to complete
- Requires relatively complex locking machinery across
binaries/tests/processes
5. A two step solution where the developer has to build the binaries
before calling the smoke tests
    - Very error prone

None of these are good. `xtask` is not bad, but doesn't enable us to run
other integration tests in other crates (e.g. the CLI)

(3) is the correct solution and has the best user experience, but it
requires nightly and I don't want to introduce that for all of our
tests.

I have chosen to do a combination of (1) and (4). You will now run the
smoketests with `cargo smoketest`. If you run `cargo test --all` (or use
`guard`) without doing `cargo smoketest` it will fall back to (4) which
compiles the executables at runtime. Running `cargo build` is the **only
way** to ensure that the executables are not stale because of the
internal fingerprint checking. Everything else is fragile not robust.

NOTE! There is no way to avoid cargo within cargo and have the smoke
tests be run as cargo tests because the modules under test must be
compiled with cargo.

# API and ABI breaking changes

Note that this is a BREAKING CHANGE to `cargo test --all`. The
smoketests are now part of `cargo test --all` unless you specifically
exclude them.

# Expected complexity level and risk

3, this is partially AI translated. We need to carefully review to
ensure the semantics have not regressed.

# Testing

<!-- Describe any testing you've done, and any testing you'd like your
reviewers to do,
so that you're confident that all the changes work as expected! -->

- [ ] <!-- maybe a test you want to do -->
- [ ] <!-- maybe a test you want a reviewer to do, so they can check it
off when they're satisfied. -->

---------

Signed-off-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com>
Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com>
Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
This commit is contained in:
Tyler Cloutier
2026-02-04 16:44:32 -05:00
committed by GitHub
parent ae15508dee
commit 9686139dbc
161 changed files with 10279 additions and 229 deletions
+2
View File
@@ -5,6 +5,8 @@ rustflags = ["--cfg", "tokio_unstable"]
bump-versions = "run -p upgrade-version --"
llm = "run --package xtask-llm-benchmark --bin llm_benchmark --"
ci = "run -p ci --"
smoketest = "run -p xtask-smoketest -- smoketest"
smoketests = "smoketest"
[target.x86_64-pc-windows-msvc]
# Use a different linker. Otherwise, the build fails with some obscure linker error that
+148 -3
View File
@@ -18,9 +18,117 @@ concurrency:
cancel-in-progress: true
jobs:
docker_smoketests:
smoketests:
needs: [lints]
name: Smoketests (${{ matrix.name }})
strategy:
matrix:
include:
- name: Linux
runner: spacetimedb-new-runner-2
- name: Windows
runner: windows-latest
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
env:
CARGO_TARGET_DIR: ${{ github.workspace }}/target
steps:
- name: Find Git ref
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
PR_NUMBER="${{ github.event.inputs.pr_number || null }}"
if test -n "${PR_NUMBER}"; then
GIT_REF="$( gh pr view --repo clockworklabs/SpacetimeDB $PR_NUMBER --json headRefName --jq .headRefName )"
else
GIT_REF="${{ github.ref }}"
fi
echo "GIT_REF=${GIT_REF}" >>"$GITHUB_ENV"
- name: Checkout sources
uses: actions/checkout@v4
with:
ref: ${{ env.GIT_REF }}
- uses: dsherret/rust-toolchain-file@v1
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: ${{ github.workspace }}
shared-key: spacetimedb
cache-on-failure: false
cache-all-crates: true
cache-workspace-crates: true
prefix-key: v1
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
# nodejs and pnpm are required for the typescript quickstart smoketest
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v4
with:
run_install: true
- name: Install psql (Windows)
if: runner.os == 'Windows'
run: choco install psql -y --no-progress
shell: powershell
- name: Update dotnet workloads
if: runner.os == 'Windows'
run: |
# Fail properly if any individual command fails
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
cd modules
# the sdk-manifests on windows-latest are messed up, so we need to update them
dotnet workload config --update-mode manifests
dotnet workload update
- name: Override NuGet packages
shell: bash
run: |
dotnet pack -c Release crates/bindings-csharp/BSATN.Runtime
dotnet pack -c Release crates/bindings-csharp/Runtime
cd sdks/csharp
./tools~/write-nuget-config.sh ../..
# This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a.
# ChatGPT suspects that this could be due to different build invocations using the same target dir,
# and this makes sense to me because we only see it in this job where we mix `cargo build -p` with
# `cargo build --manifest-path` (which apparently build different dependency trees).
# However, we've been unable to fix it so... /shrug
- name: Check v8 outputs
shell: bash
run: |
find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true
if ! [ -f "${CARGO_TARGET_DIR}"/debug/gn_out/obj/librusty_v8.a ]; then
echo "Could not find v8 output file librusty_v8.a; rebuilding manually."
cargo clean -p v8 || true
cargo build -p v8
fi
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest
- name: Run smoketests
# --test-threads=1 eliminates contention in the C# tests where they fight over bindings
# build artifacts.
# It also seemed to improve performance a fair amount (11m -> 6m)
run: cargo ci smoketests -- --test-threads=1
smoketests-python:
needs: [lints]
name: Smoketests (Python Legacy) (${{matrix.name}})
strategy:
matrix:
include:
@@ -103,14 +211,17 @@ jobs:
if: runner.os == 'Windows'
run: choco install psql -y --no-progress
shell: powershell
- name: Build crates
run: cargo build -p spacetimedb-cli -p spacetimedb-standalone -p spacetimedb-update
- name: Build and start database (Linux)
if: runner.os == 'Linux'
run: |
# Our .dockerignore omits `target`, which our CI Dockerfile needs.
rm .dockerignore
docker compose -f .github/docker-compose.yml up -d
- name: Build and start database (Windows)
if: runner.os == 'Windows'
run: |
@@ -123,14 +234,18 @@ jobs:
# the sdk-manifests on windows-latest are messed up, so we need to update them
dotnet workload config --update-mode manifests
dotnet workload update
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
if: runner.os == 'Windows'
- name: Install python deps
run: python -m pip install -r smoketests/requirements.txt
- name: Run smoketests
- name: Run Python smoketests
# Note: clear_database and replication only work in private
run: cargo ci smoketests -- ${{ matrix.smoketest_args }} -x clear_database replication teams
run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication teams
- name: Stop containers (Linux)
if: always() && runner.os == 'Linux'
run: docker compose -f .github/docker-compose.yml down
@@ -921,3 +1036,33 @@ jobs:
- name: Check global.json policy
run: cargo ci global-json-policy
warn-python-smoketests:
name: Check for Python smoketest edits
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fail if Python smoketests were modified
run: |
MERGE_BASE="$(git merge-base origin/${{ github.base_ref }} HEAD)"
PYTHON_SMOKETEST_CHANGES="$(git diff --name-only "$MERGE_BASE" HEAD -- 'smoketests/**.py')"
if [ -n "$PYTHON_SMOKETEST_CHANGES" ]; then
echo "::error::This PR modifies legacy Python smoketests. Please add new tests to the Rust smoketests in crates/smoketests/ instead."
echo ""
echo "Changed files:"
echo "$PYTHON_SMOKETEST_CHANGES"
echo ""
echo "The Python smoketests are being replaced by Rust smoketests."
echo "See crates/smoketests/DEVELOP.md for instructions on adding Rust smoketests."
exit 1
fi
echo "No Python smoketest changes detected."
+4
View File
@@ -211,6 +211,10 @@ crates/bench/spacetime.svg
crates/bench/sqlite.svg
.vs/
# .NET build artifacts
**/obj/
**/bin/
# benchmark files
out.json
old.json
Generated
+51 -3
View File
@@ -444,6 +444,14 @@ dependencies = [
"vsimd",
]
[[package]]
name = "basic-rs-template-module"
version = "0.1.0"
dependencies = [
"log",
"spacetimedb 1.12.0",
]
[[package]]
name = "benchmarks-module"
version = "0.1.0"
@@ -2041,6 +2049,12 @@ dependencies = [
"log",
]
[[package]]
name = "env_home"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "env_logger"
version = "0.10.2"
@@ -7448,7 +7462,6 @@ name = "spacetimedb-cli"
version = "1.12.0"
dependencies = [
"anyhow",
"assert_cmd",
"base64 0.21.7",
"bytes",
"cargo_metadata",
@@ -7474,7 +7487,6 @@ dependencies = [
"names",
"notify 7.0.0",
"percent-encoding",
"predicates",
"pretty_assertions",
"quick-xml 0.31.0",
"regex",
@@ -7491,7 +7503,6 @@ dependencies = [
"spacetimedb-codegen",
"spacetimedb-data-structures",
"spacetimedb-fs-utils",
"spacetimedb-guard",
"spacetimedb-jsonwebtoken",
"spacetimedb-lib 1.12.0",
"spacetimedb-paths",
@@ -8226,6 +8237,24 @@ dependencies = [
"tokio-tungstenite",
]
[[package]]
name = "spacetimedb-smoketests"
version = "1.12.0"
dependencies = [
"anyhow",
"assert_cmd",
"cargo_metadata",
"predicates",
"regex",
"serde_json",
"spacetimedb-guard",
"tempfile",
"tokio",
"tokio-postgres",
"toml 0.8.23",
"which 8.0.0",
]
[[package]]
name = "spacetimedb-snapshot"
version = "1.12.0"
@@ -10370,6 +10399,17 @@ dependencies = [
"winsafe",
]
[[package]]
name = "which"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
"env_home",
"rustix 1.1.2",
"winsafe",
]
[[package]]
name = "whoami"
version = "1.6.1"
@@ -11045,6 +11085,14 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "xtask-smoketest"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.5.50",
]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
+4
View File
@@ -1,4 +1,5 @@
[workspace]
exclude = ["crates/smoketests/modules"]
members = [
"crates/auth",
"crates/bench",
@@ -26,6 +27,7 @@ members = [
"crates/query",
"crates/sats",
"crates/schema",
"crates/smoketests",
"sdks/rust",
"sdks/unreal",
"crates/snapshot",
@@ -41,6 +43,7 @@ members = [
"modules/keynote-benchmarks",
"modules/perf-test",
"modules/module-test",
"templates/basic-rs/spacetimedb",
"templates/chat-console-rs/spacetimedb",
"modules/sdk-test",
"modules/sdk-test-connect-disconnect",
@@ -58,6 +61,7 @@ members = [
"tools/generate-client-api",
"tools/gen-bindings",
"tools/xtask-llm-benchmark",
"tools/xtask-smoketest",
"crates/bindings-typescript/test-app/server",
"crates/bindings-typescript/test-react-router-app/server",
"crates/query-builder",
-3
View File
@@ -87,9 +87,6 @@ notify.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
fs_extra.workspace = true
assert_cmd = "2"
predicates = "3"
spacetimedb-guard.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = { workspace = true }
+1
View File
@@ -88,6 +88,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
None => sats_to_json(&module_def)?,
};
// TODO: validate the JSON output
println!("{json}");
} else {
// TODO: human-readable API
-72
View File
@@ -1,72 +0,0 @@
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
#[test]
fn cli_dev_help_shows_template_option() {
let mut cmd = cargo_bin_cmd!("spacetimedb-cli");
cmd.args(["dev", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--template"))
.stdout(predicate::str::contains("-t"));
}
#[test]
fn cli_dev_accepts_template_flag() {
// This test verifies that the CLI correctly parses the --template flag.
// We use --help after the flag to avoid actually running dev mode,
// but this still validates that the flag is recognized.
let mut cmd = cargo_bin_cmd!("spacetimedb-cli");
// Running with an invalid server should fail, but not because of the template flag
cmd.args(["dev", "--template", "react", "--server", "nonexistent-server-12345"])
.assert()
.failure()
// The error should be about the server, not about an unrecognized --template flag
.stderr(
predicate::str::contains("template")
.not()
.or(predicate::str::contains("unrecognized").not()),
);
}
#[test]
fn cli_dev_accepts_short_template_flag() {
let mut cmd = cargo_bin_cmd!("spacetimedb-cli");
cmd.args(["dev", "-t", "typescript", "--server", "nonexistent-server-12345"])
.assert()
.failure()
// The error should be about the server, not about an unrecognized -t flag
.stderr(
predicate::str::contains("-t")
.not()
.or(predicate::str::contains("unrecognized").not()),
);
}
#[test]
fn cli_init_with_template_creates_project() {
// Test that `spacetime init --template` successfully creates a project
// We use init directly since dev forwards to it for template handling
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let mut cmd = cargo_bin_cmd!("spacetimedb-cli");
cmd.current_dir(temp_dir.path())
.args([
"init",
"--template",
"basic-rs",
"--local",
"--non-interactive",
"test-project",
])
.assert()
.success();
// Verify expected files were created
let project_dir = temp_dir.path().join("test-project");
assert!(
project_dir.join("spacetimedb").exists(),
"spacetimedb directory should exist"
);
assert!(project_dir.join("src").exists(), "src directory should exist");
}
-11
View File
@@ -1,11 +0,0 @@
use assert_cmd::cargo::cargo_bin_cmd;
use spacetimedb_guard::SpacetimeDbGuard;
#[test]
fn cli_can_ping_spacetimedb_on_disk() {
let spacetime = SpacetimeDbGuard::spawn_in_temp_data_dir();
let mut cmd = cargo_bin_cmd!("spacetimedb-cli");
cmd.args(["server", "ping", &spacetime.host_url.to_string()])
.assert()
.success();
}
+372 -95
View File
@@ -4,18 +4,108 @@ use std::{
env,
io::{BufRead, BufReader},
net::SocketAddr,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::{Arc, Mutex},
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex, OnceLock,
},
thread::{self, sleep},
time::{Duration, Instant},
};
/// Global counter for spawn IDs to correlate log messages across threads.
static SPAWN_COUNTER: AtomicU64 = AtomicU64::new(0);
fn next_spawn_id() -> u64 {
SPAWN_COUNTER.fetch_add(1, Ordering::SeqCst)
}
/// Returns the workspace root directory.
// TODO: Should this use something like `git rev-parse --show-toplevel` to avoid being directory-relative? Or perhaps `CARGO_WORKSPACE_DIR` is set?
fn workspace_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent() // crates/
.and_then(|p| p.parent()) // workspace root
.expect("Failed to find workspace root")
.to_path_buf()
}
/// Returns the target directory.
fn target_dir() -> PathBuf {
let workspace_root = workspace_root();
env::var("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| workspace_root.join("target"))
}
/// Returns the expected CLI binary path.
fn cli_binary_path() -> PathBuf {
let profile = if cfg!(debug_assertions) { "debug" } else { "release" };
let cli_name = if cfg!(windows) {
"spacetimedb-cli.exe"
} else {
"spacetimedb-cli"
};
target_dir().join(profile).join(cli_name)
}
/// Lazily-initialized path to the pre-built CLI binary.
static CLI_BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
/// Returns the path to the pre-built CLI binary.
///
/// **This function does NOT build anything.** The binary must already exist.
/// Use `cargo smoketest` to build binaries before running tests.
///
/// # Panics
///
/// Panics if the binary does not exist.
pub fn ensure_binaries_built() -> PathBuf {
CLI_BINARY_PATH
.get_or_init(|| {
let cli_path = cli_binary_path();
if !cli_path.exists() {
panic!(
"\n\
========================================================================\n\
ERROR: CLI binary not found at {}\n\
\n\
Smoketests require pre-built binaries. Run:\n\
\n\
cargo smoketest\n\
\n\
Or build manually:\n\
\n\
cargo build -p spacetimedb-cli -p spacetimedb-standalone\n\
========================================================================\n",
cli_path.display()
);
}
cli_path
})
.clone()
}
use reqwest::blocking::Client;
pub struct SpacetimeDbGuard {
pub child: Child,
pub host_url: String,
pub logs: Arc<Mutex<String>>,
/// The PostgreSQL wire protocol port, if enabled.
pub pg_port: Option<u16>,
/// The data directory path (for restart scenarios).
pub data_dir: PathBuf,
/// Owns the temporary data directory (if created by spawn_in_temp_data_dir).
/// When this is Some, dropping the guard will clean up the temp dir.
_data_dir_handle: Option<tempfile::TempDir>,
/// Reader thread handles for stdout/stderr - joined on drop to prevent leaks.
reader_threads: Vec<thread::JoinHandle<()>>,
use_installed_cli: bool,
}
// Remove all Cargo-provided env vars from a child process. These are set by the fact that we're running in a cargo
@@ -25,160 +115,349 @@ impl SpacetimeDbGuard {
/// Start `spacetimedb` in a temporary data directory via:
/// cargo run -p spacetimedb-cli -- start --data-dir <temp-dir> --listen-addr <addr>
pub fn spawn_in_temp_data_dir() -> Self {
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let data_dir = temp_dir.path().display().to_string();
Self::spawn_in_temp_data_dir_with_pg_port(None)
}
Self::spawn_spacetime_start(false, &["start", "--data-dir", &data_dir])
/// Start `spacetimedb` in a temporary data directory with optional PostgreSQL wire protocol.
pub fn spawn_in_temp_data_dir_with_pg_port(pg_port: Option<u16>) -> Self {
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let data_dir_path = temp_dir.path().to_path_buf();
Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir_path, Some(temp_dir))
}
/// Start `spacetimedb` in a temporary data directory via:
/// spacetime start --data-dir <temp-dir> --listen-addr <addr>
pub fn spawn_in_temp_data_dir_use_cli() -> Self {
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
let data_dir = temp_dir.path().display().to_string();
let data_dir_path = temp_dir.path().to_path_buf();
Self::spawn_spacetime_start(true, &["start", "--data-dir", &data_dir])
Self::spawn_spacetime_start_with_data_dir(true, None, data_dir_path, Some(temp_dir))
}
fn spawn_spacetime_start(use_installed_cli: bool, extra_args: &[&str]) -> Self {
// Ask SpacetimeDB/OS to allocate an ephemeral port.
// Using loopback avoids needing to "connect to 0.0.0.0".
let address = "127.0.0.1:0".to_string();
// Workspace root for `cargo run -p ...`
let workspace_dir = env!("CARGO_MANIFEST_DIR");
let mut args = vec![];
let (child, logs) = if use_installed_cli {
args.extend_from_slice(extra_args);
args.extend_from_slice(&["--listen-addr", &address]);
let cmd = Command::new("spacetime");
Self::spawn_child(cmd, env!("CARGO_MANIFEST_DIR"), &args)
} else {
Self::build_prereqs(workspace_dir);
args.extend(vec!["run", "-p", "spacetimedb-cli", "--"]);
args.extend(extra_args);
args.extend(["--listen-addr", &address]);
let cmd = Command::new("cargo");
Self::spawn_child(cmd, workspace_dir, &args)
};
// Parse the actual bound address from logs.
let listen_addr = wait_for_listen_addr(&logs, Duration::from_secs(10))
.unwrap_or_else(|| panic!("Timed out waiting for SpacetimeDB to report listen address"));
let host_url = format!("http://{}", listen_addr);
let guard = SpacetimeDbGuard { child, host_url, logs };
guard.wait_until_http_ready(Duration::from_secs(10));
guard
/// Start `spacetimedb` with an explicit data directory (for restart scenarios).
///
/// Unlike `spawn_in_temp_data_dir`, this method does not create a temporary directory.
/// The caller is responsible for managing the data directory lifetime.
pub fn spawn_with_data_dir(data_dir: PathBuf, pg_port: Option<u16>) -> Self {
Self::spawn_spacetime_start_with_data_dir(false, pg_port, data_dir, None)
}
// Ensure standalone is built before we start, if thats needed.
// This is best-effort and usually a no-op when already built.
// Also build the CLI before running it to avoid that being included in the
// timeout for readiness.
fn build_prereqs(workspace_dir: &str) {
let targets = ["spacetimedb-standalone", "spacetimedb-cli"];
for pkg in targets {
let mut cmd = Command::new("cargo");
let _ = cmd
.args(["build", "-p", pkg])
.current_dir(workspace_dir)
.status()
.unwrap_or_else(|_| panic!("failed to build {}", pkg));
fn spawn_spacetime_start_with_data_dir(
use_installed_cli: bool,
pg_port: Option<u16>,
data_dir: PathBuf,
_data_dir_handle: Option<tempfile::TempDir>,
) -> Self {
let spawn_id = next_spawn_id();
let (child, logs, host_url, reader_threads) =
Self::spawn_server(&data_dir, pg_port, spawn_id, use_installed_cli);
SpacetimeDbGuard {
child,
host_url,
logs,
pg_port,
data_dir,
_data_dir_handle,
reader_threads,
use_installed_cli,
}
}
fn spawn_child(mut cmd: Command, workspace_dir: &str, args: &[&str]) -> (Child, Arc<Mutex<String>>) {
/// Stop the server process without dropping the guard.
///
/// This kills the server process but preserves the data directory.
/// Use `restart()` to start the server again with the same data.
pub fn stop(&mut self) {
self.kill_process();
}
/// Restart the server with the same data directory.
///
/// This stops the current server process and starts a new one
/// with the same data directory, preserving all data.
pub fn restart(&mut self) {
let spawn_id = next_spawn_id();
let old_pid = self.child.id();
eprintln!("[RESTART-{:03}] Starting restart, old pid={}", spawn_id, old_pid);
self.stop();
eprintln!("[RESTART-{:03}] Old process stopped, sleeping 100ms", spawn_id);
// Brief pause to ensure system resources are fully released
sleep(Duration::from_millis(100));
eprintln!("[RESTART-{:03}] Spawning new server", spawn_id);
let (child, logs, host_url, reader_threads) =
Self::spawn_server(&self.data_dir, self.pg_port, spawn_id, self.use_installed_cli);
eprintln!(
"[RESTART-{:03}] New server ready, pid={}, url={}",
spawn_id,
child.id(),
host_url
);
self.child = child;
self.logs = logs;
self.host_url = host_url;
self.reader_threads = reader_threads;
}
/// Kills the current server process and waits for it to exit.
fn kill_process(&mut self) {
let pid = self.child.id();
eprintln!("[KILL] Killing process tree for pid={}", pid);
// Kill the process tree to ensure all child processes are terminated.
// On Windows, child.kill() only kills the direct child (spacetimedb-cli),
// leaving spacetimedb-standalone running as an orphan.
#[cfg(windows)]
{
let status = Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
eprintln!("[KILL] taskkill result for pid={}: {:?}", pid, status);
}
#[cfg(not(windows))]
{
let result = self.child.kill();
eprintln!("[KILL] kill result for pid={}: {:?}", pid, result);
}
let wait_result = self.child.wait();
eprintln!("[KILL] wait() result for pid={}: {:?}", pid, wait_result);
// Join reader threads to prevent leaks.
// The threads will exit naturally once the process is killed and pipes close.
let threads = std::mem::take(&mut self.reader_threads);
for handle in threads {
let _ = handle.join();
}
eprintln!("[KILL] Reader threads joined for pid={}", pid);
}
/// Spawns a new server process with the given data directory.
/// Returns (child, logs, host_url, reader_threads).
fn spawn_server(
data_dir: &Path,
pg_port: Option<u16>,
spawn_id: u64,
use_installed_cli: bool,
) -> (Child, Arc<Mutex<String>>, String, Vec<thread::JoinHandle<()>>) {
let cmd = if use_installed_cli {
eprintln!("[SPAWN-{:03}] START Using installed CLI", spawn_id);
Command::new("spacetime")
} else {
// Use the built CLI (common case)
let cli_path = ensure_binaries_built();
Command::new(&cli_path)
};
eprintln!(
"[SPAWN-{:03}] START data_dir={:?} pg_port={:?}",
spawn_id, data_dir, pg_port
);
let data_dir_str = data_dir.display().to_string();
let pg_port_str = pg_port.map(|p| p.to_string());
let address = "127.0.0.1:0".to_string();
let mut args = vec![
"start",
"--jwt-key-dir",
&data_dir_str,
"--data-dir",
&data_dir_str,
"--listen-addr",
&address,
];
if let Some(ref port) = pg_port_str {
args.extend(["--pg-port", port]);
}
eprintln!("[SPAWN-{:03}] Spawning child process", spawn_id);
let (child, logs, reader_threads) = Self::spawn_child(cmd, &args, spawn_id);
eprintln!("[SPAWN-{:03}] Child spawned pid={}", spawn_id, child.id());
// Wait for the server to be ready
eprintln!("[SPAWN-{:03}] Waiting for listen address", spawn_id);
let listen_addr = wait_for_listen_addr(&logs, Duration::from_secs(10), spawn_id).unwrap_or_else(|| {
// Dump diagnostic info on failure
let buf = logs.lock().unwrap();
eprintln!("[SPAWN-{:03}] TIMEOUT after 10s", spawn_id);
eprintln!(
"[SPAWN-{:03}] Captured {} bytes, {} lines",
spawn_id,
buf.len(),
buf.lines().count()
);
eprintln!(
"[SPAWN-{:03}] Contains 'Starting SpacetimeDB': {}",
spawn_id,
buf.contains("Starting SpacetimeDB")
);
// Check if process is still running
drop(buf); // Release lock before try_wait
panic!("Timed out waiting for SpacetimeDB to report listen address")
});
eprintln!("[SPAWN-{:03}] Got listen_addr={}", spawn_id, listen_addr);
let host_url = format!("http://{}", listen_addr);
// Wait until HTTP is ready
eprintln!("[SPAWN-{:03}] Waiting for HTTP ready", spawn_id);
let client = Client::new();
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
let url = format!("{}/v1/ping", host_url);
if let Ok(resp) = client.get(&url).send() {
if resp.status().is_success() {
eprintln!("[SPAWN-{:03}] HTTP ready at {}", spawn_id, host_url);
return (child, logs, host_url, reader_threads);
}
}
sleep(Duration::from_millis(50));
}
panic!("Timed out waiting for SpacetimeDB HTTP /v1/ping at {}", host_url);
}
fn spawn_child(
mut cmd: Command,
args: &[&str],
spawn_id: u64,
) -> (Child, Arc<Mutex<String>>, Vec<thread::JoinHandle<()>>) {
eprintln!("[SPAWN-{:03}] spawn_child: about to spawn", spawn_id);
let mut child = cmd
.args(args)
.current_dir(workspace_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn spacetimedb-cli");
let logs = Arc::new(Mutex::new(String::new()));
let pid = child.id();
eprintln!("[SPAWN-{:03}] spawn_child: spawned pid={}", spawn_id, pid);
// Attach stdout logger
let logs = Arc::new(Mutex::new(String::new()));
let mut reader_threads = Vec::new();
// Attach stdout logger with diagnostic logging
if let Some(stdout) = child.stdout.take() {
let logs_clone = logs.clone();
thread::spawn(move || {
let handle = thread::spawn(move || {
eprintln!("[READER-{:03}] stdout reader started for pid={}", spawn_id, pid);
let reader = BufReader::new(stdout);
let mut line_count = 0;
for line in reader.lines().map_while(Result::ok) {
line_count += 1;
// Log the first few lines and any line containing the listen address
if line_count <= 5 || line.contains("Starting SpacetimeDB") {
eprintln!("[READER-{:03}] stdout line {}: {:.100}", spawn_id, line_count, line);
}
let mut buf = logs_clone.lock().unwrap();
buf.push_str("[STDOUT] ");
buf.push_str(&line);
buf.push('\n');
}
eprintln!(
"[READER-{:03}] stdout reader ended, {} lines total",
spawn_id, line_count
);
});
reader_threads.push(handle);
}
// Attach stderr logger
// Attach stderr logger with diagnostic logging
if let Some(stderr) = child.stderr.take() {
let logs_clone = logs.clone();
thread::spawn(move || {
let handle = thread::spawn(move || {
eprintln!("[READER-{:03}] stderr reader started for pid={}", spawn_id, pid);
let reader = BufReader::new(stderr);
let mut line_count = 0;
for line in reader.lines().map_while(Result::ok) {
line_count += 1;
// Log the first few lines and any errors
if line_count <= 5 || line.contains("error") || line.contains("Error") {
eprintln!("[READER-{:03}] stderr line {}: {:.100}", spawn_id, line_count, line);
}
let mut buf = logs_clone.lock().unwrap();
buf.push_str("[STDERR] ");
buf.push_str(&line);
buf.push('\n');
}
eprintln!(
"[READER-{:03}] stderr reader ended, {} lines total",
spawn_id, line_count
);
});
reader_threads.push(handle);
}
(child, logs)
}
fn wait_until_http_ready(&self, timeout: Duration) {
let client = Client::new();
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let url = format!("{}/v1/ping", self.host_url);
if let Ok(resp) = client.get(&url).send() {
if resp.status().is_success() {
return; // Fully ready!
}
}
sleep(Duration::from_millis(50));
}
panic!("Timed out waiting for SpacetimeDB HTTP /v1/ping at {}", self.host_url);
eprintln!("[SPAWN-{:03}] spawn_child: readers attached", spawn_id);
(child, logs, reader_threads)
}
}
/// Wait for a line like:
/// "... Starting SpacetimeDB listening on 0.0.0.0:24326"
fn wait_for_listen_addr(logs: &Arc<Mutex<String>>, timeout: Duration) -> Option<SocketAddr> {
let deadline = Instant::now() + timeout;
let mut cursor = 0usize;
fn wait_for_listen_addr(logs: &Arc<Mutex<String>>, timeout: Duration, spawn_id: u64) -> Option<SocketAddr> {
let start = Instant::now();
let deadline = start + timeout;
let mut last_len = 0;
let mut last_report = Instant::now();
while Instant::now() < deadline {
let (new_text, new_len) = {
let buf = logs.lock().unwrap();
if cursor >= buf.len() {
(String::new(), buf.len())
} else {
(buf[cursor..].to_string(), buf.len())
}
};
cursor = new_len;
// Always search the entire log buffer to avoid missing lines that
// might be split across multiple reader iterations.
let buf = logs.lock().unwrap().clone();
for line in new_text.lines() {
for line in buf.lines() {
if let Some(addr) = parse_listen_addr_from_line(line) {
eprintln!("[SPAWN-{:03}] Found listen addr after {:?}", spawn_id, start.elapsed());
return Some(addr);
}
}
// Progress report every 2 seconds
let current_len = buf.len();
if last_report.elapsed() > Duration::from_secs(2) {
let delta = current_len.saturating_sub(last_len);
eprintln!(
"[SPAWN-{:03}] Waiting: {} bytes (+{}), {} lines, {:?} elapsed",
spawn_id,
current_len,
delta,
buf.lines().count(),
start.elapsed()
);
last_len = current_len;
last_report = Instant::now();
}
sleep(Duration::from_millis(25));
}
// Debug output on timeout
let buf = logs.lock().unwrap().clone();
eprintln!(
"[SPAWN-{:03}] wait_for_listen_addr TIMEOUT: {} bytes, {} lines, elapsed {:?}",
spawn_id,
buf.len(),
buf.lines().count(),
start.elapsed()
);
eprintln!(
"[SPAWN-{:03}] Contains 'Starting SpacetimeDB': {}",
spawn_id,
buf.contains("Starting SpacetimeDB")
);
// Show first 500 chars
let preview: String = buf.chars().take(500).collect();
eprintln!("[SPAWN-{:03}] First 500 chars: {:?}", spawn_id, preview);
None
}
@@ -195,9 +474,7 @@ fn parse_listen_addr_from_line(line: &str) -> Option<SocketAddr> {
impl Drop for SpacetimeDbGuard {
fn drop(&mut self) {
// Best-effort cleanup.
let _ = self.child.kill();
let _ = self.child.wait();
self.kill_process();
// Only print logs if the test is currently panicking
if std::thread::panicking() {
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "spacetimedb-smoketests"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[dependencies]
# Test utilities (needed in lib for test helpers)
spacetimedb-guard.workspace = true
tempfile.workspace = true
serde_json.workspace = true
toml.workspace = true
regex.workspace = true
anyhow.workspace = true
which = "8.0.0"
[dev-dependencies]
cargo_metadata.workspace = true
assert_cmd = "2"
predicates = "3"
tokio.workspace = true
tokio-postgres.workspace = true
[lints]
workspace = true
+98
View File
@@ -0,0 +1,98 @@
# Smoketests Development Guide
## Running Tests
### Recommended: cargo smoketest
```bash
cargo smoketest
```
This command:
1. Builds `spacetimedb-cli` and `spacetimedb-standalone` binaries
2. Runs all smoketests in parallel using nextest (or cargo test if nextest isn't installed)
To run specific tests:
```bash
cargo smoketest test_sql_format
cargo smoketest "cli::" # Run all CLI tests
```
### WARNING: Stale Binary Risk
**Smoketests use pre-built binaries and DO NOT automatically rebuild them.**
If you modify code in `spacetimedb-cli`, `spacetimedb-standalone`, or their dependencies,
you MUST rebuild before running tests:
```bash
# Option 1: Use cargo smoketest (always rebuilds first)
cargo smoketest
# Option 2: Manually rebuild, then run tests directly
cargo build -p spacetimedb-cli -p spacetimedb-standalone
cargo nextest run -p spacetimedb-smoketests
```
**If you run `cargo nextest run` or `cargo test` directly without rebuilding,
you may be testing against OLD binaries.** This can cause confusing test failures
or, worse, tests that pass when they shouldn't.
To check which binary you're testing against:
```bash
ls -la target/debug/spacetimedb-cli* # Check modification time
```
### Why This Design?
Running `cargo build` from inside parallel tests causes race conditions on Windows
where multiple processes try to replace running executables ("Access denied" errors).
Pre-building avoids this entirely.
### Alternative: cargo test
Standard `cargo test` also works, but you must rebuild first:
```bash
cargo build -p spacetimedb-cli -p spacetimedb-standalone
cargo test -p spacetimedb-smoketests
```
## Test Performance
Each test takes ~15-20s due to:
- **WASM compilation** (~12s): Each test compiles a fresh Rust module to WASM
- **Server spawn** (~2s): Each test starts its own SpacetimeDB server
- **Module publish** (~2s): Server processes and initializes the WASM module
When running tests in parallel, resource contention increases individual test times but reduces overall runtime.
## Writing Tests
See existing tests for patterns. Key points:
```rust
use spacetimedb_smoketests::Smoketest;
const MODULE_CODE: &str = r#"
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = example, public)]
pub struct Example { value: u64 }
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, value: u64) {
ctx.db.example().insert(Example { value });
}
"#;
#[test]
fn test_example() {
let test = Smoketest::builder()
.module_code(MODULE_CODE)
.build();
test.call("add", &["42"]).unwrap();
test.assert_sql("SELECT * FROM example", "value\n-----\n42");
}
```
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
# Nested workspace for pre-compiled smoketest modules.
# This workspace is excluded from the root workspace and built separately
# during the smoketest warmup phase.
#
# All modules here are compiled to WASM once during warmup, then reused
# by tests without per-test compilation overhead.
[workspace]
resolver = "3"
members = [
# Filtering and query tests
"filtering",
"dml",
# Views tests
"views-basic",
# "views-broken-namespace" - intentionally broken, uses runtime compilation
# "views-broken-return-type" - intentionally broken, uses runtime compilation
"views-sql",
"views-auto-migrate",
"views-auto-migrate-updated",
"views-drop-view",
"views-trapped",
"views-recovered",
"views-subscribe",
"views-query",
# Security and permissions
"rls",
"rls-no-filter",
"rls-with-filter",
"permissions-private",
"permissions-lifecycle",
# Call/procedure tests
"call-reducer-procedure",
"call-empty",
# SQL format tests
"sql-format",
"pg-wire",
# Scheduled reducer tests
"schedule-cancel",
"schedule-subscribe",
"schedule-procedure",
"schedule-volatile",
# Module lifecycle tests
"describe",
"modules-basic",
"modules-breaking",
"modules-add-table",
"upload-module-2",
"hotswap-basic",
"hotswap-updated",
# Index tests
"add-remove-index",
"add-remove-index-indexed",
# Panic/error handling
"panic",
"panic-error",
# Restart tests
"restart-person",
"restart-connected-client",
# Connection tests
"connect-disconnect",
"confirmed-reads",
"delete-database",
"client-connection-reject",
"client-connection-disconnect-panic",
# Misc tests
"namespaces",
"new-user-flow",
"module-nested-op",
# "fail-initial-publish-broken" - intentionally broken, uses runtime compilation
"fail-initial-publish-fixed",
# Auto-increment tests (all 10 integer types in one module each)
"autoinc-basic",
"autoinc-unique",
]
[workspace.dependencies]
spacetimedb = { path = "../../../crates/bindings", features = ["unstable"] }
log = "0.4"
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-add-remove-index-indexed"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,22 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = t1)]
pub struct T1 { #[index(btree)] id: u64 }
#[spacetimedb::table(name = t2)]
pub struct T2 { #[index(btree)] id: u64 }
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
for id in 0..1_000 {
ctx.db.t1().insert(T1 { id });
ctx.db.t2().insert(T2 { id });
}
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext) {
let id = 1_001;
ctx.db.t1().insert(T1 { id });
ctx.db.t2().insert(T2 { id });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-add-remove-index"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,15 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = t1)]
pub struct T1 { id: u64 }
#[spacetimedb::table(name = t2)]
pub struct T2 { id: u64 }
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
for id in 0..1_000 {
ctx.db.t1().insert(T1 { id });
ctx.db.t2().insert(T2 { id });
}
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-autoinc-basic"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = { path = "../../../../crates/bindings" }
log = "0.4"
paste = "1.0"
@@ -0,0 +1,33 @@
#![allow(non_camel_case_types)]
use spacetimedb::{log, ReducerContext, Table};
macro_rules! autoinc_basic {
($($ty:ident),*) => {
$(
paste::paste! {
#[spacetimedb::table(name = [<person_ $ty>])]
pub struct [<Person_ $ty>] {
#[auto_inc]
key_col: $ty,
name: String,
}
#[spacetimedb::reducer]
pub fn [<add_ $ty>](ctx: &ReducerContext, name: String, expected_value: $ty) {
let value = ctx.db.[<person_ $ty>]().insert([<Person_ $ty>] { key_col: 0, name });
assert_eq!(value.key_col, expected_value);
}
#[spacetimedb::reducer]
pub fn [<say_hello_ $ty>](ctx: &ReducerContext) {
for person in ctx.db.[<person_ $ty>]().iter() {
log::info!("Hello, {}:{}!", person.key_col, person.name);
}
log::info!("Hello, World!");
}
}
)*
};
}
autoinc_basic!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-autoinc-unique"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = { path = "../../../../crates/bindings" }
log = "0.4"
paste = "1.0"
@@ -0,0 +1,43 @@
#![allow(non_camel_case_types)]
use spacetimedb::{log, ReducerContext, Table};
use std::error::Error;
macro_rules! autoinc_unique {
($($ty:ident),*) => {
$(
paste::paste! {
#[spacetimedb::table(name = [<person_ $ty>])]
pub struct [<Person_ $ty>] {
#[auto_inc]
#[unique]
key_col: $ty,
#[unique]
name: String,
}
#[spacetimedb::reducer]
pub fn [<add_new_ $ty>](ctx: &ReducerContext, name: String) -> Result<(), Box<dyn Error>> {
let value = ctx.db.[<person_ $ty>]().try_insert([<Person_ $ty>] { key_col: 0, name })?;
log::info!("Assigned Value: {} -> {}", value.key_col, value.name);
Ok(())
}
#[spacetimedb::reducer]
pub fn [<update_ $ty>](ctx: &ReducerContext, name: String, new_id: $ty) {
ctx.db.[<person_ $ty>]().name().delete(&name);
let _value = ctx.db.[<person_ $ty>]().insert([<Person_ $ty>] { key_col: new_id, name });
}
#[spacetimedb::reducer]
pub fn [<say_hello_ $ty>](ctx: &ReducerContext) {
for person in ctx.db.[<person_ $ty>]().iter() {
log::info!("Hello, {}:{}!", person.key_col, person.name);
}
log::info!("Hello, World!");
}
}
)*
};
}
autoinc_unique!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-call-empty"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,4 @@
#[spacetimedb::table(name = person)]
pub struct Person {
name: String,
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-call-reducer-procedure"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,16 @@
use spacetimedb::{log, ProcedureContext, ReducerContext};
#[spacetimedb::table(name = person)]
pub struct Person {
name: String,
}
#[spacetimedb::reducer]
pub fn say_hello(_ctx: &ReducerContext) {
log::info!("Hello, World!");
}
#[spacetimedb::procedure]
pub fn return_person(_ctx: &mut ProcedureContext) -> Person {
return Person { name: "World".to_owned() };
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-client-connection-disconnect-panic"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,23 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = all_u8s, public)]
pub struct AllU8s {
number: u8,
}
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
for i in u8::MIN..=u8::MAX {
ctx.db.all_u8s().insert(AllU8s { number: i });
}
}
#[spacetimedb::reducer(client_connected)]
pub fn identity_connected(_ctx: &ReducerContext) -> Result<(), String> {
Ok(())
}
#[spacetimedb::reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
panic!("This should be called, but the `st_client` row should still be deleted")
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-client-connection-reject"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,23 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = all_u8s, public)]
pub struct AllU8s {
number: u8,
}
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
for i in u8::MIN..=u8::MAX {
ctx.db.all_u8s().insert(AllU8s { number: i });
}
}
#[spacetimedb::reducer(client_connected)]
pub fn identity_connected(_ctx: &ReducerContext) -> Result<(), String> {
Err("Rejecting connection from client".to_string())
}
#[spacetimedb::reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
panic!("This should never be called, since we reject all connections!")
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-confirmed-reads"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,11 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person, public)]
pub struct Person {
name: String,
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { name });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-connect-disconnect"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,16 @@
use spacetimedb::{log, ReducerContext};
#[spacetimedb::reducer(client_connected)]
pub fn connected(_ctx: &ReducerContext) {
log::info!("_connect called");
}
#[spacetimedb::reducer(client_disconnected)]
pub fn disconnected(_ctx: &ReducerContext) {
log::info!("disconnect called");
}
#[spacetimedb::reducer]
pub fn say_hello(_ctx: &ReducerContext) {
log::info!("Hello, World!");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-delete-database"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,37 @@
use spacetimedb::{ReducerContext, Table, duration};
#[spacetimedb::table(name = counter, public)]
pub struct Counter {
#[primary_key]
id: u64,
val: u64
}
#[spacetimedb::table(name = scheduled_counter, public, scheduled(inc, at = sched_at))]
pub struct ScheduledCounter {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
sched_at: spacetimedb::ScheduleAt,
}
#[spacetimedb::reducer]
pub fn inc(ctx: &ReducerContext, arg: ScheduledCounter) {
if let Some(mut counter) = ctx.db.counter().id().find(arg.scheduled_id) {
counter.val += 1;
ctx.db.counter().id().update(counter);
} else {
ctx.db.counter().insert(Counter {
id: arg.scheduled_id,
val: 1,
});
}
}
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
ctx.db.scheduled_counter().insert(ScheduledCounter {
scheduled_id: 0,
sched_at: duration!(100ms).into(),
});
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-describe"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,19 @@
use spacetimedb::{log, ReducerContext, Table};
#[spacetimedb::table(name = person)]
pub struct Person {
name: String,
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-dml"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
+4
View File
@@ -0,0 +1,4 @@
#[spacetimedb::table(name = t, public)]
pub struct T {
name: String,
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-fail-initial-publish-broken"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,10 @@
use spacetimedb::{client_visibility_filter, Filter};
#[spacetimedb::table(name = person, public)]
pub struct Person {
name: String,
}
#[client_visibility_filter]
// Bug: `Person` is the wrong table name, should be `person`.
const HIDE_PEOPLE_EXCEPT_ME: Filter = Filter::Sql("SELECT * FROM Person WHERE name = 'me'");
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-fail-initial-publish-fixed"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,9 @@
use spacetimedb::{client_visibility_filter, Filter};
#[spacetimedb::table(name = person, public)]
pub struct Person {
name: String,
}
#[client_visibility_filter]
const HIDE_PEOPLE_EXCEPT_ME: Filter = Filter::Sql("SELECT * FROM person WHERE name = 'me'");
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-filtering"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,182 @@
use spacetimedb::{log, Identity, ReducerContext, Table};
#[spacetimedb::table(name = person)]
pub struct Person {
#[unique]
id: i32,
name: String,
#[unique]
nick: String,
}
#[spacetimedb::reducer]
pub fn insert_person(ctx: &ReducerContext, id: i32, name: String, nick: String) {
ctx.db.person().insert(Person { id, name, nick} );
}
#[spacetimedb::reducer]
pub fn insert_person_twice(ctx: &ReducerContext, id: i32, name: String, nick: String) {
// We'd like to avoid an error due to a set-semantic error.
let name2 = format!("{name}2");
ctx.db.person().insert(Person { id, name, nick: nick.clone()} );
match ctx.db.person().try_insert(Person { id, name: name2, nick: nick.clone()}) {
Ok(_) => {},
Err(_) => {
log::info!("UNIQUE CONSTRAINT VIOLATION ERROR: id = {}, nick = {}", id, nick)
}
}
}
#[spacetimedb::reducer]
pub fn delete_person(ctx: &ReducerContext, id: i32) {
ctx.db.person().id().delete(&id);
}
#[spacetimedb::reducer]
pub fn find_person(ctx: &ReducerContext, id: i32) {
match ctx.db.person().id().find(&id) {
Some(person) => log::info!("UNIQUE FOUND: id {}: {}", id, person.name),
None => log::info!("UNIQUE NOT FOUND: id {}", id),
}
}
#[spacetimedb::reducer]
pub fn find_person_read_only(ctx: &ReducerContext, id: i32) {
let ctx = ctx.as_read_only();
match ctx.db.person().id().find(&id) {
Some(person) => log::info!("UNIQUE FOUND: id {}: {}", id, person.name),
None => log::info!("UNIQUE NOT FOUND: id {}", id),
}
}
#[spacetimedb::reducer]
pub fn find_person_by_name(ctx: &ReducerContext, name: String) {
for person in ctx.db.person().iter().filter(|p| p.name == name) {
log::info!("UNIQUE FOUND: id {}: {} aka {}", person.id, person.name, person.nick);
}
}
#[spacetimedb::reducer]
pub fn find_person_by_nick(ctx: &ReducerContext, nick: String) {
match ctx.db.person().nick().find(&nick) {
Some(person) => log::info!("UNIQUE FOUND: id {}: {}", person.id, person.nick),
None => log::info!("UNIQUE NOT FOUND: nick {}", nick),
}
}
#[spacetimedb::reducer]
pub fn find_person_by_nick_read_only(ctx: &ReducerContext, nick: String) {
let ctx = ctx.as_read_only();
match ctx.db.person().nick().find(&nick) {
Some(person) => log::info!("UNIQUE FOUND: id {}: {}", person.id, person.nick),
None => log::info!("UNIQUE NOT FOUND: nick {}", nick),
}
}
#[spacetimedb::table(name = nonunique_person)]
pub struct NonuniquePerson {
#[index(btree)]
id: i32,
name: String,
is_human: bool,
}
#[spacetimedb::reducer]
pub fn insert_nonunique_person(ctx: &ReducerContext, id: i32, name: String, is_human: bool) {
ctx.db.nonunique_person().insert(NonuniquePerson { id, name, is_human } );
}
#[spacetimedb::reducer]
pub fn find_nonunique_person(ctx: &ReducerContext, id: i32) {
for person in ctx.db.nonunique_person().id().filter(&id) {
log::info!("NONUNIQUE FOUND: id {}: {}", id, person.name)
}
}
#[spacetimedb::reducer]
pub fn find_nonunique_person_read_only(ctx: &ReducerContext, id: i32) {
let ctx = ctx.as_read_only();
for person in ctx.db.nonunique_person().id().filter(&id) {
log::info!("NONUNIQUE FOUND: id {}: {}", id, person.name)
}
}
#[spacetimedb::reducer]
pub fn find_nonunique_humans(ctx: &ReducerContext) {
for person in ctx.db.nonunique_person().iter().filter(|p| p.is_human) {
log::info!("HUMAN FOUND: id {}: {}", person.id, person.name);
}
}
#[spacetimedb::reducer]
pub fn find_nonunique_non_humans(ctx: &ReducerContext) {
for person in ctx.db.nonunique_person().iter().filter(|p| !p.is_human) {
log::info!("NON-HUMAN FOUND: id {}: {}", person.id, person.name);
}
}
// Ensure that [Identity] is filterable and a legal unique column.
#[spacetimedb::table(name = identified_person)]
struct IdentifiedPerson {
#[unique]
identity: Identity,
name: String,
}
fn identify(id_number: u64) -> Identity {
let mut bytes = [0u8; 32];
bytes[..8].clone_from_slice(&id_number.to_le_bytes());
Identity::from_byte_array(bytes)
}
#[spacetimedb::reducer]
fn insert_identified_person(ctx: &ReducerContext, id_number: u64, name: String) {
let identity = identify(id_number);
ctx.db.identified_person().insert(IdentifiedPerson { identity, name });
}
#[spacetimedb::reducer]
fn find_identified_person(ctx: &ReducerContext, id_number: u64) {
let identity = identify(id_number);
match ctx.db.identified_person().identity().find(&identity) {
Some(person) => log::info!("IDENTIFIED FOUND: {}", person.name),
None => log::info!("IDENTIFIED NOT FOUND"),
}
}
// Ensure that indices on non-unique columns behave as we expect.
#[spacetimedb::table(name = indexed_person)]
struct IndexedPerson {
#[unique]
id: i32,
given_name: String,
#[index(btree)]
surname: String,
}
#[spacetimedb::reducer]
fn insert_indexed_person(ctx: &ReducerContext, id: i32, given_name: String, surname: String) {
ctx.db.indexed_person().insert(IndexedPerson { id, given_name, surname });
}
#[spacetimedb::reducer]
fn delete_indexed_person(ctx: &ReducerContext, id: i32) {
ctx.db.indexed_person().id().delete(&id);
}
#[spacetimedb::reducer]
fn find_indexed_people(ctx: &ReducerContext, surname: String) {
for person in ctx.db.indexed_person().surname().filter(&surname) {
log::info!("INDEXED FOUND: id {}: {}, {}", person.id, person.surname, person.given_name);
}
}
#[spacetimedb::reducer]
fn find_indexed_people_read_only(ctx: &ReducerContext, surname: String) {
let ctx = ctx.as_read_only();
for person in ctx.db.indexed_person().surname().filter(&surname) {
log::info!("INDEXED FOUND: id {}: {}, {}", person.id, person.surname, person.given_name);
}
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-hotswap-basic"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,14 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person, public)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
}
#[spacetimedb::reducer]
pub fn add_person(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { id: 0, name });
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-hotswap-updated"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,25 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person, public)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
}
#[spacetimedb::reducer]
pub fn add_person(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { id: 0, name });
}
#[spacetimedb::table(name = pet, public)]
pub struct Pet {
#[primary_key]
species: String,
}
#[spacetimedb::reducer]
pub fn add_pet(ctx: &ReducerContext, species: String) {
ctx.db.pet().insert(Pet { species });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-module-nested-op"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,39 @@
use spacetimedb::{log, ReducerContext, Table};
#[spacetimedb::table(name = account)]
pub struct Account {
name: String,
#[unique]
id: i32,
}
#[spacetimedb::table(name = friends)]
pub struct Friends {
friend_1: i32,
friend_2: i32,
}
#[spacetimedb::reducer]
pub fn create_account(ctx: &ReducerContext, account_id: i32, name: String) {
ctx.db.account().insert(Account { id: account_id, name } );
}
#[spacetimedb::reducer]
pub fn add_friend(ctx: &ReducerContext, my_id: i32, their_id: i32) {
// Make sure our friend exists
for account in ctx.db.account().iter() {
if account.id == their_id {
ctx.db.friends().insert(Friends { friend_1: my_id, friend_2: their_id });
return;
}
}
}
#[spacetimedb::reducer]
pub fn say_friends(ctx: &ReducerContext) {
for friendship in ctx.db.friends().iter() {
let friend1 = ctx.db.account().id().find(&friendship.friend_1).unwrap();
let friend2 = ctx.db.account().id().find(&friendship.friend_2).unwrap();
log::info!("{} is friends with {}", friend1.name, friend2.name);
}
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-modules-add-table"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,19 @@
use spacetimedb::{log, ReducerContext};
#[spacetimedb::table(name = person)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
}
#[spacetimedb::table(name = pets)]
pub struct Pet {
species: String,
}
#[spacetimedb::reducer]
pub fn are_we_updated_yet(_ctx: &ReducerContext) {
log::info!("MODULE UPDATED");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-modules-basic"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,22 @@
use spacetimedb::{log, ReducerContext, Table};
#[spacetimedb::table(name = person)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { id: 0, name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-modules-breaking"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,8 @@
#[spacetimedb::table(name = person)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
age: u8,
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-namespaces"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,34 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person, public)]
pub struct Person {
name: String,
}
#[spacetimedb::reducer(init)]
pub fn init(_ctx: &ReducerContext) {
// Called when the module is initially published
}
#[spacetimedb::reducer(client_connected)]
pub fn identity_connected(_ctx: &ReducerContext) {
// Called everytime a new client connects
}
#[spacetimedb::reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
// Called everytime a client disconnects
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-new-user-flow"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,19 @@
use spacetimedb::{log, ReducerContext, Table};
#[spacetimedb::table(name = person)]
pub struct Person {
name: String
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-panic-error"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,6 @@
use spacetimedb::ReducerContext;
#[spacetimedb::reducer]
fn fail(_ctx: &ReducerContext) -> Result<(), String> {
Err("oopsie :(".into())
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-panic"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,18 @@
use spacetimedb::{log, ReducerContext};
use std::cell::RefCell;
thread_local! {
static X: RefCell<u32> = RefCell::new(0);
}
#[spacetimedb::reducer]
fn first(_ctx: &ReducerContext) {
X.with(|x| {
let _x = x.borrow_mut();
panic!()
})
}
#[spacetimedb::reducer]
fn second(_ctx: &ReducerContext) {
X.with(|x| *x.borrow_mut());
log::info!("Test Passed");
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-permissions-lifecycle"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,8 @@
#[spacetimedb::reducer(init)]
fn lifecycle_init(_ctx: &spacetimedb::ReducerContext) {}
#[spacetimedb::reducer(client_connected)]
fn lifecycle_client_connected(_ctx: &spacetimedb::ReducerContext) {}
#[spacetimedb::reducer(client_disconnected)]
fn lifecycle_client_disconnected(_ctx: &spacetimedb::ReducerContext) {}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-permissions-private"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,22 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = secret, private)]
pub struct Secret {
answer: u8,
}
#[spacetimedb::table(name = common_knowledge, public)]
pub struct CommonKnowledge {
thing: String,
}
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
ctx.db.secret().insert(Secret { answer: 42 });
}
#[spacetimedb::reducer]
pub fn do_thing(ctx: &ReducerContext, thing: String) {
ctx.db.secret().insert(Secret { answer: 20 });
ctx.db.common_knowledge().insert(CommonKnowledge { thing });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-pg-wire"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,159 @@
use spacetimedb::sats::{i256, u256};
use spacetimedb::{ConnectionId, Identity, ReducerContext, SpacetimeType, Table, Timestamp, TimeDuration, Uuid};
#[derive(Copy, Clone)]
#[spacetimedb::table(name = t_ints, public)]
pub struct TInts {
i8: i8,
i16: i16,
i32: i32,
i64: i64,
i128: i128,
i256: i256,
}
#[spacetimedb::table(name = t_ints_tuple, public)]
pub struct TIntsTuple {
tuple: TInts,
}
#[derive(Copy, Clone)]
#[spacetimedb::table(name = t_uints, public)]
pub struct TUints {
u8: u8,
u16: u16,
u32: u32,
u64: u64,
u128: u128,
u256: u256,
}
#[spacetimedb::table(name = t_uints_tuple, public)]
pub struct TUintsTuple {
tuple: TUints,
}
#[derive(Clone)]
#[spacetimedb::table(name = t_others, public)]
pub struct TOthers {
bool: bool,
f32: f32,
f64: f64,
str: String,
bytes: Vec<u8>,
identity: Identity,
connection_id: ConnectionId,
timestamp: Timestamp,
duration: TimeDuration,
uuid: Uuid,
}
#[spacetimedb::table(name = t_others_tuple, public)]
pub struct TOthersTuple {
tuple: TOthers
}
#[derive(SpacetimeType, Debug, Clone, Copy)]
pub enum Action {
Inactive,
Active,
}
#[derive(SpacetimeType, Debug, Clone, Copy)]
pub enum Color {
Gray(u8),
}
#[derive(Copy, Clone)]
#[spacetimedb::table(name = t_simple_enum, public)]
pub struct TSimpleEnum {
id: u32,
action: Action,
}
#[spacetimedb::table(name = t_enum, public)]
pub struct TEnum {
id: u32,
color: Color,
}
#[spacetimedb::table(name = t_nested, public)]
pub struct TNested {
en: TEnum,
se: TSimpleEnum,
ints: TInts,
}
#[derive(Clone)]
#[spacetimedb::table(name = t_enums)]
pub struct TEnums {
bool_opt: Option<bool>,
bool_result: Result<bool, String>,
action: Action,
}
#[spacetimedb::table(name = t_enums_tuple)]
pub struct TEnumsTuple {
tuple: TEnums,
}
#[spacetimedb::reducer]
pub fn test(ctx: &ReducerContext) {
let tuple = TInts {
i8: -25,
i16: -3224,
i32: -23443,
i64: -2344353,
i128: -234434897853,
i256: (-234434897853i128).into(),
};
let ints = tuple;
ctx.db.t_ints().insert(tuple);
ctx.db.t_ints_tuple().insert(TIntsTuple { tuple });
let tuple = TUints {
u8: 105,
u16: 1050,
u32: 83892,
u64: 48937498,
u128: 4378528978889,
u256: 4378528978889u128.into(),
};
ctx.db.t_uints().insert(tuple);
ctx.db.t_uints_tuple().insert(TUintsTuple { tuple });
let tuple = TOthers {
bool: true,
f32: 594806.58906,
f64: -3454353.345389043278459,
str: "This is spacetimedb".to_string(),
bytes: vec!(1, 2, 3, 4, 5, 6, 7),
identity: Identity::ONE,
connection_id: ConnectionId::ZERO,
timestamp: Timestamp::UNIX_EPOCH,
duration: TimeDuration::from_micros(1000 * 10000),
uuid: Uuid::NIL,
};
ctx.db.t_others().insert(tuple.clone());
ctx.db.t_others_tuple().insert(TOthersTuple { tuple });
ctx.db.t_simple_enum().insert(TSimpleEnum { id: 1, action: Action::Inactive });
ctx.db.t_simple_enum().insert(TSimpleEnum { id: 2, action: Action::Active });
ctx.db.t_enum().insert(TEnum { id: 1, color: Color::Gray(128) });
ctx.db.t_nested().insert(TNested {
en: TEnum { id: 1, color: Color::Gray(128) },
se: TSimpleEnum { id: 2, action: Action::Active },
ints,
});
let tuple = TEnums {
bool_opt: Some(true),
bool_result: Ok(false),
action: Action::Active,
};
ctx.db.t_enums().insert(tuple.clone());
ctx.db.t_enums_tuple().insert(TEnumsTuple { tuple });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-restart-connected-client"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,34 @@
use log::info;
use spacetimedb::{ConnectionId, Identity, ReducerContext, Table};
#[spacetimedb::table(name = connected_client)]
pub struct ConnectedClient {
identity: Identity,
connection_id: ConnectionId,
}
#[spacetimedb::reducer(client_connected)]
fn on_connect(ctx: &ReducerContext) {
ctx.db.connected_client().insert(ConnectedClient {
identity: ctx.sender,
connection_id: ctx.connection_id.expect("sender connection id unset"),
});
}
#[spacetimedb::reducer(client_disconnected)]
fn on_disconnect(ctx: &ReducerContext) {
let sender_identity = &ctx.sender;
let sender_connection_id = ctx.connection_id.as_ref().expect("sender connection id unset");
let match_client = |row: &ConnectedClient| {
&row.identity == sender_identity && &row.connection_id == sender_connection_id
};
if let Some(client) = ctx.db.connected_client().iter().find(match_client) {
ctx.db.connected_client().delete(client);
}
}
#[spacetimedb::reducer]
fn print_num_connected(ctx: &ReducerContext) {
let n = ctx.db.connected_client().count();
info!("CONNECTED CLIENTS: {n}")
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-restart-person"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,22 @@
use spacetimedb::{log, ReducerContext, Table};
#[spacetimedb::table(name = person, index(name = name_idx, btree(columns = [name])))]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u32,
name: String,
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { id: 0, name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-rls-no-filter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,15 @@
use spacetimedb::{Identity, ReducerContext, Table};
#[spacetimedb::table(name = users, public)]
pub struct Users {
name: String,
identity: Identity,
}
#[spacetimedb::reducer]
pub fn add_user(ctx: &ReducerContext, name: String) {
ctx.db.users().insert(Users {
name,
identity: ctx.sender,
});
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-rls-with-filter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,19 @@
use spacetimedb::{Identity, ReducerContext, Table};
#[spacetimedb::table(name = users, public)]
pub struct Users {
name: String,
identity: Identity,
}
#[spacetimedb::client_visibility_filter]
const USER_FILTER: spacetimedb::Filter =
spacetimedb::Filter::Sql("SELECT * FROM users WHERE identity = :sender");
#[spacetimedb::reducer]
pub fn add_user(ctx: &ReducerContext, name: String) {
ctx.db.users().insert(Users {
name,
identity: ctx.sender,
});
}
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-rls"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
+17
View File
@@ -0,0 +1,17 @@
use spacetimedb::{Identity, ReducerContext, Table};
#[spacetimedb::table(name = users, public)]
pub struct Users {
name: String,
identity: Identity,
}
#[spacetimedb::client_visibility_filter]
const USER_FILTER: spacetimedb::Filter = spacetimedb::Filter::Sql(
"SELECT * FROM users WHERE identity = :sender"
);
#[spacetimedb::reducer]
pub fn add_user(ctx: &ReducerContext, name: String) {
ctx.db.users().insert(Users { name, identity: ctx.sender });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-schedule-cancel"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,37 @@
use spacetimedb::{duration, log, ReducerContext, Table};
#[spacetimedb::reducer(init)]
fn init(ctx: &ReducerContext) {
let schedule = ctx.db.scheduled_reducer_args().insert(ScheduledReducerArgs {
num: 1,
scheduled_id: 0,
scheduled_at: duration!(100ms).into(),
});
ctx.db.scheduled_reducer_args().scheduled_id().delete(&schedule.scheduled_id);
let schedule = ctx.db.scheduled_reducer_args().insert(ScheduledReducerArgs {
num: 2,
scheduled_id: 0,
scheduled_at: duration!(1000ms).into(),
});
do_cancel(ctx, schedule.scheduled_id);
}
#[spacetimedb::table(name = scheduled_reducer_args, public, scheduled(reducer))]
pub struct ScheduledReducerArgs {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
scheduled_at: spacetimedb::ScheduleAt,
num: i32,
}
#[spacetimedb::reducer]
fn do_cancel(ctx: &ReducerContext, schedule_id: u64) {
ctx.db.scheduled_reducer_args().scheduled_id().delete(&schedule_id);
}
#[spacetimedb::reducer]
fn reducer(_ctx: &ReducerContext, args: ScheduledReducerArgs) {
log::info!("the reducer ran: {}", args.num);
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-schedule-procedure"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,37 @@
use spacetimedb::{duration, log, ProcedureContext, ReducerContext, Table, Timestamp};
#[spacetimedb::table(name = scheduled_table, public, scheduled(my_procedure, at = sched_at))]
pub struct ScheduledTable {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
sched_at: spacetimedb::ScheduleAt,
prev: Timestamp,
}
#[spacetimedb::reducer]
fn schedule_procedure(ctx: &ReducerContext) {
ctx.db.scheduled_table().insert(ScheduledTable {
prev: Timestamp::from_micros_since_unix_epoch(0),
scheduled_id: 2,
sched_at: Timestamp::from_micros_since_unix_epoch(0).into(),
});
}
#[spacetimedb::reducer]
fn schedule_repeated_procedure(ctx: &ReducerContext) {
ctx.db.scheduled_table().insert(ScheduledTable {
prev: Timestamp::from_micros_since_unix_epoch(0),
scheduled_id: 1,
sched_at: duration!(100ms).into(),
});
}
#[spacetimedb::procedure]
pub fn my_procedure(ctx: &mut ProcedureContext, arg: ScheduledTable) {
log::info!(
"Invoked: ts={:?}, delta={:?}",
ctx.timestamp,
ctx.timestamp.duration_since(arg.prev)
);
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-schedule-subscribe"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,25 @@
use spacetimedb::{log, duration, ReducerContext, Table, Timestamp};
#[spacetimedb::table(name = scheduled_table, public, scheduled(my_reducer, at = sched_at))]
pub struct ScheduledTable {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
sched_at: spacetimedb::ScheduleAt,
prev: Timestamp,
}
#[spacetimedb::reducer]
fn schedule_reducer(ctx: &ReducerContext) {
ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_unix_epoch(0), scheduled_id: 2, sched_at: Timestamp::from_micros_since_unix_epoch(0).into(), });
}
#[spacetimedb::reducer]
fn schedule_repeated_reducer(ctx: &ReducerContext) {
ctx.db.scheduled_table().insert(ScheduledTable { prev: Timestamp::from_micros_since_unix_epoch(0), scheduled_id: 1, sched_at: duration!(100ms).into(), });
}
#[spacetimedb::reducer]
pub fn my_reducer(ctx: &ReducerContext, arg: ScheduledTable) {
log::info!("Invoked: ts={:?}, delta={:?}", ctx.timestamp, ctx.timestamp.duration_since(arg.prev));
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-schedule-volatile"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,16 @@
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = my_table, public)]
pub struct MyTable {
x: String,
}
#[spacetimedb::reducer]
fn do_schedule(_ctx: &ReducerContext) {
spacetimedb::volatile_nonatomic_schedule_immediate!(do_insert("hello".to_owned()));
}
#[spacetimedb::reducer]
fn do_insert(ctx: &ReducerContext, x: String) {
ctx.db.my_table().insert(MyTable { x });
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-sql-format"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,122 @@
use spacetimedb::sats::{i256, u256};
use spacetimedb::{ConnectionId, Identity, ReducerContext, Table, Timestamp, TimeDuration, SpacetimeType, Uuid};
#[derive(Copy, Clone)]
#[spacetimedb::table(name = t_ints)]
pub struct TInts {
i8: i8,
i16: i16,
i32: i32,
i64: i64,
i128: i128,
i256: i256,
}
#[spacetimedb::table(name = t_ints_tuple)]
pub struct TIntsTuple {
tuple: TInts,
}
#[derive(Copy, Clone)]
#[spacetimedb::table(name = t_uints)]
pub struct TUints {
u8: u8,
u16: u16,
u32: u32,
u64: u64,
u128: u128,
u256: u256,
}
#[spacetimedb::table(name = t_uints_tuple)]
pub struct TUintsTuple {
tuple: TUints,
}
#[derive(Clone)]
#[spacetimedb::table(name = t_others)]
pub struct TOthers {
bool: bool,
f32: f32,
f64: f64,
str: String,
bytes: Vec<u8>,
identity: Identity,
connection_id: ConnectionId,
timestamp: Timestamp,
duration: TimeDuration,
uuid: Uuid,
}
#[spacetimedb::table(name = t_others_tuple)]
pub struct TOthersTuple {
tuple: TOthers
}
#[derive(SpacetimeType, Debug, Clone, Copy)]
pub enum Action {
Inactive,
Active,
}
#[derive(Clone)]
#[spacetimedb::table(name = t_enums)]
pub struct TEnums {
bool_opt: Option<bool>,
bool_result: Result<bool, String>,
action: Action,
}
#[spacetimedb::table(name = t_enums_tuple)]
pub struct TEnumsTuple {
tuple: TEnums,
}
#[spacetimedb::reducer]
pub fn test(ctx: &ReducerContext) {
let tuple = TInts {
i8: -25,
i16: -3224,
i32: -23443,
i64: -2344353,
i128: -234434897853,
i256: (-234434897853i128).into(),
};
ctx.db.t_ints().insert(tuple);
ctx.db.t_ints_tuple().insert(TIntsTuple { tuple });
let tuple = TUints {
u8: 105,
u16: 1050,
u32: 83892,
u64: 48937498,
u128: 4378528978889,
u256: 4378528978889u128.into(),
};
ctx.db.t_uints().insert(tuple);
ctx.db.t_uints_tuple().insert(TUintsTuple { tuple });
let tuple = TOthers {
bool: true,
f32: 594806.58906,
f64: -3454353.345389043278459,
str: "This is spacetimedb".to_string(),
bytes: vec!(1, 2, 3, 4, 5, 6, 7),
identity: Identity::ONE,
connection_id: ConnectionId::ZERO,
timestamp: Timestamp::UNIX_EPOCH,
duration: TimeDuration::ZERO,
uuid: Uuid::NIL,
};
ctx.db.t_others().insert(tuple.clone());
ctx.db.t_others_tuple().insert(TOthersTuple { tuple });
let tuple = TEnums {
bool_opt: Some(true),
bool_result: Ok(false),
action: Action::Active,
};
ctx.db.t_enums().insert(tuple.clone());
ctx.db.t_enums_tuple().insert(TEnumsTuple { tuple });
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-upload-module-2"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,24 @@
use spacetimedb::{log, duration, ReducerContext, Table, Timestamp};
#[spacetimedb::table(name = scheduled_message, public, scheduled(my_repeating_reducer))]
pub struct ScheduledMessage {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
scheduled_at: spacetimedb::ScheduleAt,
prev: Timestamp,
}
#[spacetimedb::reducer(init)]
fn init(ctx: &ReducerContext) {
ctx.db.scheduled_message().insert(ScheduledMessage {
prev: ctx.timestamp,
scheduled_id: 0,
scheduled_at: duration!(100ms).into(),
});
}
#[spacetimedb::reducer]
pub fn my_repeating_reducer(ctx: &ReducerContext, arg: ScheduledMessage) {
log::info!("Invoked: ts={:?}, delta={:?}", ctx.timestamp, ctx.timestamp.duration_since(arg.prev));
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-views-auto-migrate-updated"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
@@ -0,0 +1,15 @@
use spacetimedb::ViewContext;
#[derive(Copy, Clone)]
#[spacetimedb::table(name = player_state)]
pub struct PlayerState {
#[primary_key]
id: u64,
#[index(btree)]
level: u64,
}
#[spacetimedb::view(name = player, public)]
pub fn player(ctx: &ViewContext) -> Option<PlayerState> {
ctx.db.player_state().id().find(2u64)
}
@@ -0,0 +1,11 @@
[package]
name = "smoketest-module-views-auto-migrate"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
@@ -0,0 +1,15 @@
use spacetimedb::ViewContext;
#[derive(Copy, Clone)]
#[spacetimedb::table(name = player_state)]
pub struct PlayerState {
#[primary_key]
id: u64,
#[index(btree)]
level: u64,
}
#[spacetimedb::view(name = player, public)]
pub fn player(ctx: &ViewContext) -> Option<PlayerState> {
ctx.db.player_state().id().find(1u64)
}
@@ -0,0 +1,12 @@
[package]
name = "smoketest-module-views-basic"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb.workspace = true
log.workspace = true
@@ -0,0 +1,15 @@
use spacetimedb::ViewContext;
#[derive(Copy, Clone)]
#[spacetimedb::table(name = player_state)]
pub struct PlayerState {
#[primary_key]
id: u64,
#[index(btree)]
level: u64,
}
#[spacetimedb::view(name = player, public)]
pub fn player(ctx: &ViewContext) -> Option<PlayerState> {
ctx.db.player_state().id().find(0u64)
}

Some files were not shown because too many files have changed in this diff Show More