Skip to content

Commit 06a2161

Browse files
authored
Merge branch 'master' into tyler/update-nativeaot-llvm-infrastructure
2 parents 006b564 + bb3e1b5 commit 06a2161

61 files changed

Lines changed: 1554 additions & 1312 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ ahash = { version = "0.8", default-features = false, features = ["std"] }
153153
anyhow = "1.0.68"
154154
anymap = "0.12"
155155
arrayvec = "0.7.2"
156+
async-channel = "2.5"
156157
async-stream = "0.3.6"
157158
async-trait = "0.1.68"
158159
axum = { version = "0.7", features = ["tracing"] }

TESTING.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# A brief overview of SpacetimeDB testing
2+
## From the perspective of a client SDK or module library developer
3+
## By pgoldman 2025-06-25, updated 2026-04-14
4+
5+
SpacetimeDB has good test coverage, but it is rather haphazardly spread across several suites.
6+
Some of the reasons for this are historical, and some have to do with our using multiple repositories.
7+
This document is an attempt to describe the test suites which would be useful to someone
8+
working on a new client SDK or module bindings library in a new language,
9+
or attempting to move an existing client SDK or module bindings library from an external repository in-tree.
10+
11+
## The SDK tests
12+
13+
`crates/testing/src/sdk.rs` defines a test harness which was originally designed for testing client SDKs.
14+
The basic flow of a test using this harness is:
15+
16+
- Build and freshly publish a module to construct a short-lived initially-empty database.
17+
- Use that module to run client codegen via `spacetime generate` into a client project.
18+
- Compile that client project with the newly-generated bindings.
19+
- Run the client project as a subprocess, passing the database name or `Identity` in an environment variable.
20+
- The client process connects to the database,
21+
runs whatever tests it likes,
22+
writes to stdout and/or stderr as it goes,
23+
then uses its exit code to report whether the test was successful or not.
24+
- If the subprocess's exit is non-zero, the test is treated as a failure,
25+
and the subprocess's stdout and stderr are reported.
26+
27+
This framework has since been used more generally for integration testing.
28+
In particular, we maintain equivalent Rust, C#, TypeScript, and C++ modules in the `modules/sdk-test*` family,
29+
and run the Rust SDK client project at `sdks/rust/tests/test-client` against them through `sdks/rust/tests/test.rs`.
30+
We similarly maintain `modules/sdk-test-connect-disconnect*` modules
31+
which run against `sdks/rust/tests/connect_disconnect_client`.
32+
There are also related SDK-harness-driven suites for event tables, procedures, and views,
33+
using modules such as `modules/sdk-test-event-table`, `modules/sdk-test-procedure*`, and `modules/sdk-test-view*`.
34+
The Unreal SDK also uses the same underlying harness through `sdks/unreal/tests/sdk_unreal_harness.rs`.
35+
36+
The harness is designed to support running multiple tests in parallel with the same client project,
37+
running client codegen exactly once per test suite run.
38+
This unfortunately still conflicts with our use of the suite to test that modules in different languages behave the same,
39+
as each test suite invocation will only run `spacetime generate` against one module language at a time,
40+
never all of them in the same run.
41+
42+
### Testing a new module library
43+
44+
If you are developing a new module bindings library, and wish to add it to the SDK test suite
45+
so that the existing client test projects will run against it:
46+
47+
1. Create `modules/sdk-test-XX` and `modules/sdk-test-connect-disconnect-XX`, where `XX` is some mnemonic for your language.
48+
Populate these with module code which defines all of the same tables and reducers
49+
as `modules/sdk-test` and `modules/sdk-test-connect-disconnect` respectively.
50+
Take care to use the same names, including casing, for tables, columns, indexes, reducers and other database objects.
51+
2. Modify `sdks/rust/tests/test.rs` to add an additional call to `declare_tests_with_suffix!` at the bottom,
52+
like `declare_tests_with_suffix!(xxlang, "-XX")`, if that driver is the right place for the new language.
53+
Some capabilities now live in separate suites in that file, such as procedures and views,
54+
so you may need to wire those up separately as well.
55+
3. Run the tests with `cargo test -p spacetimedb-sdk --test test`.
56+
57+
### Testing a new client SDK
58+
59+
If you are developing a new client SDK, and wish to use the SDK test harness and existing modules
60+
so that it will run against `modules/sdk-test` and `modules/sdk-test-connect-disconnect`:
61+
62+
1. Find somewhere sensible to define test projects `test-client` and `connect_disconnect_client` for your client SDK language.
63+
If your client SDK is in-tree, put these within its directory, following the existing layout under `sdks/rust/tests/` or `sdks/unreal/tests/`.
64+
2. Use `spacetime generate` manually, or via the harness, to generate those projects' `module_bindings`.
65+
3. Populate those projects with client code
66+
matching `sdks/rust/tests/test-client` and `sdks/rust/tests/connect_disconnect_client` respectively.
67+
- Connect to SpacetimeDB running at `http://localhost:3000`.
68+
- Connect to the database whose name is in the environment variable `SPACETIME_SDK_TEST_DB_NAME`.
69+
- For `test-client`, take a test name as a command-line argument in `argv[1]`, and dispatch to the appropriate test to run.
70+
- For `connect_disconnect_client`, there is only one test.
71+
- The Rust code jumps through some hoops to do assertions about asynchronous events with timeouts,
72+
using an abstraction called the `TestCounter` defined in `sdks/rust/tests/test-counter`.
73+
This is effectively a semaphore with a timeout.
74+
You may or may not need to replicate this behavior.
75+
4. Create integration tests in the SDK crate which construct `spacetimedb_testing::sdk::Test` objects,
76+
following `sdks/rust/tests/test.rs` or `sdks/unreal/tests/test.rs` as a template.
77+
5. Define `#[test]` tests for each test case you have implemented,
78+
which construct `spacetimedb_testing::sdk::Test` objects containing the various subcommand strings to run your client project,
79+
then call `.run()` on them.
80+
81+
### Adding a new test case
82+
83+
If you want to add a new test case to the SDK test suite, to test some new or yet-untested functionality
84+
of either the module libraries or client SDKs:
85+
86+
1. If necessary, add new tables and/or reducers to `modules/sdk-test` and friends which exercise the behavior you want to test.
87+
2. Add a new function, `exec_foo`, to the appropriate client project,
88+
such as `sdks/rust/tests/test-client/src/lib.rs`,
89+
which connects to the database, subscribes to tables and invokes reducers as appropriate,
90+
and performs assertions about the events it observes.
91+
3. Add a branch to that client's dispatch logic,
92+
such as the `match` in `sdks/rust/tests/test-client/src/main.rs`,
93+
which matches the test name `foo` and dispatches to call your `exec_foo` function.
94+
4. Add a `#[test]` test function to the relevant test driver,
95+
such as `sdks/rust/tests/test.rs`,
96+
which does `make_test("foo").run()`, where `"foo"` is the test name you chose in step 3.
97+
5. Repeat steps 2 through 4 for any other client projects which ought to cover the same behavior.
98+
6. Run the new test with the relevant `cargo test` command for that SDK.
99+
100+
## Schema parity tests
101+
102+
`crates/schema/tests/ensure_same_schema.rs` is a separate but important companion to the SDK tests.
103+
It compares the extracted schemas of equivalent modules across languages,
104+
and is often the first place where casing, indexes, primary keys, or other schema details drift apart.
105+
As of writing, it covers the `benchmarks`, `module-test`, `sdk-test`, and `sdk-test-connect-disconnect` families.
106+
107+
If you add or update a cross-language module family,
108+
it is worth considering whether it should also be covered here.
109+
110+
## The smoketests
111+
112+
`crates/smoketests/` defines an integration and regression test suite using a Rust harness.
113+
These are useful primarily for testing the SpacetimeDB CLI, but can also be used to exercise publish flows,
114+
documentation, and other end-to-end behavior.
115+
116+
The smoketest harness is still primarily oriented around Rust modules,
117+
and it does not use the same client-project machinery as the SDK harness.
118+
It could be extended to do more in that direction, but that may not be worth the effort.
119+
As of writing, the smoketest suite includes dedicated coverage such as:
120+
121+
- `crates/smoketests/tests/smoketests/csharp_module.rs`, which smoke-tests C# module compilation.
122+
- `crates/smoketests/tests/smoketests/quickstart.rs`, which replays the quickstart guide for Rust and C#.
123+
- `crates/smoketests/DEVELOP.md`, which documents how to run and write these tests.
124+
125+
One practical note is that the smoketests use prebuilt `spacetimedb-cli` and `spacetimedb-standalone` binaries,
126+
so if you modify those crates or their dependencies, you should rebuild before running the suite.
127+
128+
## Standalone integration test
129+
130+
The `spacetimedb-testing` crate has an integration test file, `crates/testing/tests/standalone_integration_test.rs`.
131+
The tests in this file publish `modules/module-test`, `modules/module-test-cs`, `modules/module-test-ts`, and `modules/module-test-cpp`,
132+
then invoke reducers or procedures in them and inspect their logs to verify that the behavior is expected.
133+
These tests do not exercise the entire functionality of `module-test`,
134+
but by virtue of publishing it do assert that it is syntactically valid and that it compiles.
135+
136+
To add a new module library to the Standalone integration test suite:
137+
138+
1. Create `modules/module-test-XX`, where `XX` is some mnemonic for your language.
139+
2. Populate this with module code which defines all of the same tables and reducers
140+
as the existing `module-test` family.
141+
If you notice any discrepancies between the existing languages, those parts may be compiled but not run,
142+
and so you are free to ignore them.
143+
3. Modify `crates/testing/tests/standalone_integration_test.rs` to define new `#[test] #[serial]` test functions
144+
which use your new `module-test-XX` module to do the same operations as the existing tests.
145+
4. Run the tests with `cargo test -p spacetimedb-testing --test standalone_integration_test`.

crates/bindings-typescript/tests/query_error_message.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,19 @@ describe('query builder diagnostics', () => {
7171
'Cannot combine predicates from different table scopes with and/or.';
7272
const messageHint = 'move extra predicates to .where(...)';
7373

74+
// This test invokes the TypeScript compiler directly, so it uses an explicit timeout for CI variability.
7475
it('reports a clear message for free-floating and(...) in semijoin predicates', () => {
7576
const { status, output } = runTypecheck('and(l.id.eq(r.id), r.id.eq(5))');
7677
expect(status).not.toBe(0);
7778
expect(output).toContain(messageStart);
7879
expect(output).toContain(messageHint);
79-
});
80+
}, 15000);
8081

82+
// This test invokes the TypeScript compiler directly, so it uses an explicit timeout for CI variability.
8183
it('reports a clear message for method-style .and(...) in semijoin predicates', () => {
8284
const { status, output } = runTypecheck('l.id.eq(r.id).and(r.id.eq(5))');
8385
expect(status).not.toBe(0);
8486
expect(output).toContain(messageStart);
8587
expect(output).toContain(messageHint);
86-
});
88+
}, 15000);
8789
});

crates/cli/src/subcommands/delete.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
4545
let force = args.get_flag("force");
4646

4747
let identity = database_identity(&config, &resolved.database, server).await?;
48+
let delete_target = if resolved.database == identity.to_string() {
49+
identity.to_string()
50+
} else {
51+
format!("{} ({identity})", resolved.database)
52+
};
53+
54+
if !y_or_n(
55+
force,
56+
&format!("Are you sure you want to delete database {delete_target}? This action cannot be undone."),
57+
)? {
58+
println!("Aborting");
59+
return Ok(());
60+
}
61+
4862
let host_url = config.get_host_url(server)?;
4963
let request_path = format!("{host_url}/v1/database/{identity}");
5064
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
@@ -58,7 +72,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
5872
if !force {
5973
print_database_tree_info(&confirm.database_tree).await?;
6074
}
61-
if y_or_n(force, "Do you want to proceed deleting above databases?")? {
75+
if force || y_or_n(false, "Do you want to proceed deleting above databases?")? {
6276
send_request(&client, &request_path, &auth_header, Some(confirm.confirmation_token))
6377
.await?
6478
.error_for_status()?;

crates/cli/src/subcommands/list.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::util::UNSTABLE_WARNING;
66
use crate::Config;
77
use anyhow::Context;
88
use clap::{ArgMatches, Command};
9+
use futures::future::join_all;
910
use serde::Deserialize;
1011
use spacetimedb_lib::Identity;
1112
use tabled::{
@@ -24,12 +25,14 @@ pub fn cli() -> Command {
2425

2526
#[derive(Deserialize)]
2627
struct DatabasesResult {
27-
pub identities: Vec<IdentityRow>,
28+
pub identities: Vec<Identity>,
2829
}
2930

30-
#[derive(Tabled, Deserialize)]
31-
#[serde(transparent)]
32-
struct IdentityRow {
31+
#[derive(Tabled)]
32+
struct DatabaseRow {
33+
#[tabled(rename = "Database Name(s)")]
34+
pub db_names: String,
35+
#[tabled(rename = "Identity")]
3336
pub db_identity: Identity,
3437
}
3538

@@ -58,15 +61,32 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
5861
.context("unable to retrieve databases for identity")?;
5962

6063
if !result.identities.is_empty() {
61-
let mut table = Table::new(result.identities);
64+
let databases = assemble_rows(&config, server, result.identities).await?;
65+
let mut table = Table::new(databases);
6266
table
6367
.with(Style::psql())
6468
.with(Modify::new(Columns::first()).with(Alignment::left()));
65-
println!("Associated database identities for {identity}:\n");
69+
println!("Associated databases for user {identity}:\n");
6670
println!("{table}");
6771
} else {
6872
println!("No databases found for {identity}.");
6973
}
7074

7175
Ok(())
7276
}
77+
78+
async fn assemble_rows(
79+
config: &Config,
80+
server: Option<&str>,
81+
identities: Vec<Identity>,
82+
) -> anyhow::Result<Vec<DatabaseRow>> {
83+
let lookups = identities.into_iter().map(|db_identity| async move {
84+
let response = util::spacetime_reverse_dns(config, &db_identity.to_string(), server).await?;
85+
let db_names: Vec<_> = response.names.into_iter().map(|name| name.to_string()).collect();
86+
Ok(DatabaseRow {
87+
db_names: db_names.join(", "),
88+
db_identity,
89+
})
90+
});
91+
join_all(lookups).await.into_iter().collect::<anyhow::Result<Vec<_>>>()
92+
}

crates/client-api-messages/DEVELOP.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ spacetime generate -p spacetimedb-cli --lang <SDK lang> \
1919
--out-dir <sdk WebSocket schema bindings dir> \
2020
--module-def ws_schema_v2.json
2121
```
22+
23+
Note, the v3 WebSocket protocol does not have a separate schema.
24+
It reuses the v2 message schema and only changes the websocket binary framing.
25+
In v2 for example, a WebSocket frame contained a single BSATN-encoded v2 message,
26+
but in v3, a single WebSocket frame may contain a batch of one or more v2 messages.

crates/client-api-messages/src/websocket.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
pub mod common;
1818
pub mod v1;
1919
pub mod v2;
20+
pub mod v3;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//! Binary framing for websocket protocol v3.
2+
//!
3+
//! Unlike v2, v3 does not define a new outer message schema.
4+
//! A single binary websocket payload contains one or more BSATN-encoded
5+
//! [`crate::websocket::v2::ClientMessage`] values from client to server,
6+
//! or one or more consecutive BSATN-encoded [`crate::websocket::v2::ServerMessage`]
7+
//! values from server to client.
8+
//!
9+
//! Client and server may coalesce multiple messages into one websocket payload,
10+
//! or send them separately, regardless of what the other one does,
11+
//! so long as logical order is preserved.
12+
13+
pub const BIN_PROTOCOL: &str = "v3.bsatn.spacetimedb";

0 commit comments

Comments
 (0)