Skip to content

Commit fba4744

Browse files
committed
Simplify and comment test harness code
1 parent 263b33e commit fba4744

491 files changed

Lines changed: 1008 additions & 998 deletions

File tree

Some content is hidden

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

crates/testing/src/sdk.rs

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,22 @@ pub struct Test {
7474
client_project: String,
7575

7676
/// A language suitable for the `spacetime generate` CLI command.
77+
///
78+
/// The string `"unrealcpp"` is recognized and treated differently here
79+
/// because code-generation takes different arguments for Unreal client projects.
80+
/// Tests written for the Unreal client SDK must specify exactly `"unrealcpp"`,
81+
/// not any of the aliases the SpacetimeDB CLI's `generate` command would accept.
7782
generate_language: String,
7883

7984
/// A relative path within the `client_project` to place the module bindings.
8085
///
81-
/// Usually `src/module_bindings`
86+
/// Usually `src/module_bindings`.
87+
///
88+
/// For Unreal tests (i.e. when `generate_language == "unrealcpp"`),
89+
/// this is instead the Unreal module name, and so should be a non-path string.
90+
/// In this case, it will usually be `"TestClient"`.
8291
generate_subdir: String,
8392

84-
/// Unreal-specific: the target Unreal module name for codegen (e.g., "TestClient").
85-
/// Required when `generate_language == "unrealcpp"`. Ignored otherwise.
86-
unreal_module_name: Option<String>,
87-
8893
/// A shell command to compile the client project.
8994
///
9095
/// Will run with access to the env var `SPACETIME_SDK_TEST_CLIENT_PROJECT`
@@ -103,6 +108,10 @@ pub const TEST_MODULE_PROJECT_ENV_VAR: &str = "SPACETIME_SDK_TEST_MODULE_PROJECT
103108
pub const TEST_DB_NAME_ENV_VAR: &str = "SPACETIME_SDK_TEST_DB_NAME";
104109
pub const TEST_CLIENT_PROJECT_ENV_VAR: &str = "SPACETIME_SDK_TEST_CLIENT_PROJECT";
105110

111+
fn language_is_unreal(language: &str) -> bool {
112+
language.eq_ignore_ascii_case("unrealcpp")
113+
}
114+
106115
impl Test {
107116
pub fn builder() -> TestBuilder {
108117
TestBuilder::default()
@@ -112,27 +121,12 @@ impl Test {
112121

113122
let wasm_file = compile_module(&self.module_name);
114123

115-
// Determine if this is the Unreal SDK
116-
let is_unreal = self.generate_language.eq_ignore_ascii_case("unrealcpp");
117-
118-
// For Unreal: require unreal_module_name and treat client_project as --uproject-dir
119-
let unreal_module_name_ref = if is_unreal {
120-
Some(
121-
self.unreal_module_name
122-
.as_deref()
123-
.expect("unrealcpp requires `unreal_module_name` to be set on Test"),
124-
)
125-
} else {
126-
None
127-
};
128-
129124
generate_bindings(
130125
&paths,
131126
&self.generate_language,
132127
&wasm_file,
133128
&self.client_project,
134129
&self.generate_subdir,
135-
unreal_module_name_ref,
136130
);
137131

138132
compile_client(&self.compile_command, &self.client_project);
@@ -162,33 +156,49 @@ fn random_module_name() -> String {
162156
.collect()
163157
}
164158

159+
/// Memoize computing `body` based on `key` by storing the result in a [`HashMap`].
160+
///
161+
/// The hash map is protected by a [`Mutex`].
162+
/// Only a single operator may be computing a value at a time.
163+
/// Computing the values must not be re-entrant / recursive.
164+
///
165+
/// The key(s) of the hash map must already be in scope as variables.
166+
///
167+
/// The keys may be either a single variable or a tuple of variables.
168+
///
169+
/// E.g.:
170+
///
171+
/// ```rust
172+
/// fn add_one(n: i32) -> i32 {
173+
/// memoized!(|n: i32| -> i32 { n + 1 })
174+
/// }
175+
///
176+
/// fn add_together(n: i32, m: i32) -> i32 {
177+
/// memoized!(|(n, m): (i32, i32)| -> i32 { n + m })
178+
/// }
179+
/// ```
180+
///
181+
/// The key types must be `'static`, `Clone`, `Eq` and `Hash`, as they'll be stored in a [`HashMap`].
182+
///
183+
/// Used in this file primarily for running expensive and side-effecting subprocesses
184+
/// like compilation or code generation.
165185
macro_rules! memoized {
166-
// Unit arm: no clone, silence unused key.
167-
(|$key:ident: $key_ty:ty| -> () $body:block) => {{
168-
static MEMOIZED: Mutex<Option<HashMap<$key_ty, ()>>> = Mutex::new(None);
169-
{
170-
let mut map = MEMOIZED.lock().unwrap(); // guard lives for the whole block
171-
map.get_or_insert_default().entry($key).or_insert_with_key(|__k| {
172-
let $key = __k;
173-
let _ = &$key;
174-
$body
175-
});
176-
}
186+
// Recursive case: rewrite a single `key` to be a 1-tuple `(key,)`.
187+
(|$key:ident: $key_ty:ty| -> $value_ty:ty $body:block) => {{
188+
memoized!(|($key,): ($key_ty,)| -> $value_ty $body)
177189
}};
178190

179-
// Value arm: clone while guard is still alive.
180-
(|$key:ident: $key_ty:ty| -> $value_ty:ty $body:block) => {{
191+
// Base case: keys are a tuple.
192+
(|($($key_tuple:ident),* $(,)?): $key_ty:ty| -> $value_ty:ty $body:block) => {{
181193
static MEMOIZED: Mutex<Option<HashMap<$key_ty, $value_ty>>> = Mutex::new(None);
182-
let cloned = {
183-
let mut map = MEMOIZED.lock().unwrap(); // guard lives for the whole block
184-
let v = map.get_or_insert_default().entry($key).or_insert_with_key(|__k| {
185-
let $key = __k;
186-
let _ = &$key;
187-
$body
188-
});
189-
v.clone()
190-
};
191-
cloned
194+
195+
MEMOIZED
196+
.lock()
197+
.unwrap()
198+
.get_or_insert_default()
199+
.entry(($($key_tuple,)*))
200+
.or_insert_with_key(|($($key_tuple,)*)| -> $value_ty { $body })
201+
.clone()
192202
}};
193203
}
194204

@@ -226,6 +236,24 @@ fn publish_module(paths: &SpacetimePaths, wasm_file: &str) -> String {
226236
name
227237
}
228238

239+
/// Run `spacetime generate` to generate client bindings into the `client_project`.
240+
///
241+
/// `language` should be a string suitable for the `--lang` argument to `spacetime generate`.
242+
/// `"unrealcpp"` is special-cased to account for the CLI taking different arguments.
243+
/// Tests of the Unreal client SDK must use exactly that string, not any alias accepted by the CLI.
244+
///
245+
/// `wasm_file` is a path to a compiled WASM blob, as returned by [`compile_module`].
246+
///
247+
/// `client_project` and `generate_subdir` will be the values set in the [`Test`].
248+
/// These have different semantics depending on whether `language` is `"unrealcpp"`.
249+
///
250+
/// For Unreal SDK tests, the `client_project` should be the directory which contains the `.uproject` file,
251+
/// and `generate_subdir` should be the Unreal module name.
252+
///
253+
/// For non-unreal SDK tests, the `client_project` may be an arbitrary path,
254+
/// and the `generate_subdir` an arbitrary relative path within it.
255+
/// These will be combined as `"{client_project}/{generate_subdir}"` to produce the `--out-dir`.
256+
///
229257
/// Note: this function is memoized to ensure we only run `spacetime generate` once for each target directory.
230258
///
231259
/// Without this lock, if multiple `Test`s ran concurrently in the same process
@@ -259,41 +287,36 @@ fn generate_bindings(
259287
paths: &SpacetimePaths,
260288
language: &str,
261289
wasm_file: &str,
262-
client_project: &str, // For non-Unreal: base out dir. For Unreal: .uproject root dir instead.
263-
generate_subdir: &str, // Ignored for Unreal.
264-
module_name: Option<&str>, // Required for Unreal: the Unreal module to generate into.
290+
client_project: &str,
291+
generate_subdir: &str,
265292
) {
266-
let is_unreal = language.eq_ignore_ascii_case("unrealcpp");
267-
let generate_dir = format!("{client_project}/{generate_subdir}");
268-
269-
// Memoize on the *actual* output target to avoid redundant runs.
270-
let memo_key = if is_unreal {
271-
format!("unreal::{client_project}::{:?}", module_name)
272-
} else {
273-
format!("generic::{generate_dir}")
274-
};
275-
276-
memoized!(|memo_key: String| -> () {
277-
if !is_unreal {
278-
create_dir_all(&generate_dir).unwrap();
279-
}
293+
// We need these to be owned `String`s so we can memoize on them.
294+
let client_project = client_project.to_owned();
295+
let generate_subdir = generate_subdir.to_owned();
280296

281-
let mut args: Vec<&str> = vec!["generate", "--lang", language];
297+
// Codegen is side-effecting and doesn't meaningfully return a Rust value,
298+
// so our memoization has unit as the value.
299+
// This makes it run at most once for each key.
300+
memoized!(|(client_project, generate_subdir): (String, String)| -> () {
301+
let mut args: Vec<&str> = vec!["generate", "--lang", language, "--bin-path", wasm_file];
282302

283-
// Prefer --project-path/--bin-path behavior you already have; here we show --bin-path.
284-
// If you dynamically choose between them elsewhere, keep that logic and just insert the Unreal flags.
285-
args.extend_from_slice(&["--bin-path", wasm_file]);
303+
let generate_dir: String;
286304

287-
if is_unreal {
288-
let module = module_name.expect("unrealcpp requires --module-name");
289-
args.extend_from_slice(&["--module-name", module]);
305+
// `generate --lang unrealcpp` takes different arguments from non-Unreal languages
306+
// to account for some quirks of Unreal project structure.
307+
if language_is_unreal(language) {
308+
// For unreal, we use `client_project` as the uproject directory,
309+
// and `generate_subdir` as the module name.
290310
args.extend_from_slice(&["--uproject-dir", client_project]);
311+
args.extend_from_slice(&["--module-name", generate_subdir]);
291312
} else {
313+
generate_dir = format!("{client_project}/{generate_subdir}");
314+
create_dir_all(&generate_dir).unwrap();
292315
args.extend_from_slice(&["--out-dir", &generate_dir]);
293316
}
294317

295318
invoke_cli(paths, &args);
296-
});
319+
})
297320
}
298321

299322
fn split_command_string(command: &str) -> (String, Vec<String>) {
@@ -374,8 +397,7 @@ pub struct TestBuilder {
374397
module_name: Option<String>,
375398
client_project: Option<String>,
376399
generate_language: Option<String>,
377-
generate_subdir: Option<String>, // Ignored for unrealcpp
378-
unreal_module_name: Option<String>, // Required for unrealcpp
400+
generate_subdir: Option<String>,
379401
compile_command: Option<String>,
380402
run_command: Option<String>,
381403
}
@@ -419,7 +441,7 @@ impl TestBuilder {
419441
// Unreal-only: names the Unreal module into which bindings are generated.
420442
pub fn with_unreal_module(self, unreal_module_name: impl Into<String>) -> Self {
421443
TestBuilder {
422-
unreal_module_name: Some(unreal_module_name.into()),
444+
generate_subdir: Some(unreal_module_name.into()),
423445
..self
424446
}
425447
}
@@ -442,26 +464,14 @@ impl TestBuilder {
442464
let generate_language = self
443465
.generate_language
444466
.expect("Supply a client language using TestBuilder::with_language");
445-
let is_unreal = generate_language.eq_ignore_ascii_case("unrealcpp");
446467

447468
// For non-Unreal: require generate_subdir as before.
448469
// For Unreal: ignore generate_subdir entirely, but still populate with a harmless placeholder.
449-
let generate_subdir = if is_unreal {
450-
String::from("_unreal_ignored_")
451-
} else {
452-
self.generate_subdir
453-
.expect("Supply a module_bindings subdirectory using TestBuilder::with_bindings_dir")
454-
};
455-
456-
// For Unreal: require unreal_module_name.
457-
let unreal_module_name = if is_unreal {
458-
Some(
459-
self.unreal_module_name
460-
.expect("Supply Unreal module using TestBuilder::with_unreal_module for unrealcpp"),
461-
)
470+
let generate_subdir = self.generate_subdir.expect(if language_is_unreal(&generate_language) {
471+
"Supply an Unreal module name using TestBuilder::with_unreal_module"
462472
} else {
463-
None
464-
};
473+
"Supply a module_bindings subdirectory using TestBuilder::with_bindings_dir"
474+
});
465475

466476
Test {
467477
name: self.name.expect("Supply a test name using TestBuilder::with_name"),
@@ -473,7 +483,6 @@ impl TestBuilder {
473483
.expect("Supply a client project directory using TestBuilder::with_client"),
474484
generate_language,
475485
generate_subdir,
476-
unreal_module_name,
477486
compile_command: self
478487
.compile_command
479488
.expect("Supply a compile command using TestBuilder::with_compile_command"),

0 commit comments

Comments
 (0)