diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee387669506..e70ea917018 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,10 +30,6 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - - name: Install CLI tool - run: | - cargo install --path crates/cli - - name: Create /stdb dir run: | sudo mkdir /stdb diff --git a/Cargo.lock b/Cargo.lock index a13f0f788b2..01e7921f965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4447,6 +4447,7 @@ name = "spacetimedb-testing" version = "0.1.0" dependencies = [ "anyhow", + "clap 4.3.10", "duct", "lazy_static", "rand 0.8.5", diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index d1ee274515f..824360f7171 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -850,6 +850,13 @@ impl Config { } } + pub fn new_with_localhost() -> Self { + Self { + home: RawConfig::new_with_localhost(), + proj: RawConfig::default(), + } + } + pub fn save(&self) { let config_path = if let Some(config_path) = std::env::var_os("SPACETIME_CONFIG_FILE") { PathBuf::from(&config_path) diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index b98929ad89a..88d746bcc27 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -11,6 +11,7 @@ spacetimedb-standalone = { path = "../standalone", version = "0.6.1" } spacetimedb-client-api = { path = "../client-api", version = "0.6.1" } anyhow.workspace = true +clap.workspace = true serde_json.workspace = true tokio.workspace = true wasmbin.workspace = true diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 6e47d5e80c0..d58e0268cb6 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -1,4 +1,6 @@ +use clap::Command; use spacetimedb::config::{FilesLocal, SpacetimeDbFiles}; +use spacetimedb_cli::Config; use std::env; pub mod modules; @@ -17,3 +19,21 @@ pub fn set_key_env_vars(paths: &FilesLocal) { set_if_not_exist("SPACETIMEDB_JWT_PUB_KEY", paths.public_key()); set_if_not_exist("SPACETIMEDB_JWT_PRIV_KEY", paths.private_key()); } + +pub fn invoke_cli(args: &[&str]) { + lazy_static::lazy_static! { + static ref RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + static ref COMMAND: Command = Command::new("spacetime").no_binary_name(true).subcommands(spacetimedb_cli::get_subcommands()); + static ref CONFIG: Config = Config::new_with_localhost(); + } + + let args = COMMAND.clone().get_matches_from(args); + let (cmd, args) = args.subcommand().expect("Could not split subcommand and args"); + + RUNTIME + .block_on(spacetimedb_cli::exec_subcommand((*CONFIG).clone(), cmd, args)) + .unwrap() +} diff --git a/crates/testing/src/sdk.rs b/crates/testing/src/sdk.rs index 68fbc6dde1d..ae7a1add6dc 100644 --- a/crates/testing/src/sdk.rs +++ b/crates/testing/src/sdk.rs @@ -1,108 +1,27 @@ -use duct::{cmd, Handle}; +use duct::cmd; use lazy_static::lazy_static; use rand::distributions::{Alphanumeric, DistString}; +use std::thread::JoinHandle; use std::{collections::HashSet, fs::create_dir_all, sync::Mutex}; +use crate::invoke_cli; use crate::modules::{module_path, CompiledModule}; use std::path::Path; -struct StandaloneProcess { - handle: Handle, - num_using: usize, -} - -impl StandaloneProcess { - fn start() -> Self { - let handle = cmd!("spacetime", "start") - .stderr_to_stdout() - .stdout_capture() - .unchecked() - .start() - .expect("Failed to run `spacetime start`"); - - StandaloneProcess { handle, num_using: 1 } +pub fn ensure_standalone_process() { + lazy_static! { + static ref JOIN_HANDLE: Mutex>> = + Mutex::new(Some(std::thread::spawn(|| invoke_cli(&["start"])))); } - fn stop(&mut self) -> anyhow::Result<()> { - assert!(self.num_using == 0); - - self.handle.kill()?; + let mut join_handle = JOIN_HANDLE.lock().unwrap(); - Ok(()) - } - - fn running_or_err(&self) -> anyhow::Result<()> { - if let Some(output) = self - .handle - .try_wait() - .expect("Error from spacetime standalone subprocess") - { - let code = output.status; - let output = String::from_utf8_lossy(&output.stdout); - Err(anyhow::anyhow!( - "spacetime start exited unexpectedly. Exit status: {}. Output:\n{}", - code, - output, - )) - } else { - Ok(()) - } - } - - fn add_user(&mut self) -> anyhow::Result<()> { - self.running_or_err()?; - self.num_using += 1; - Ok(()) - } - - /// Returns true if the process was stopped because no one is using it. - fn sub_user(&mut self) -> anyhow::Result { - self.num_using -= 1; - if self.num_using == 0 { - self.stop()?; - Ok(true) - } else { - Ok(false) - } - } -} - -static STANDALONE_PROCESS: Mutex> = Mutex::new(None); - -/// An RAII handle on the `STANDALONE_PROCESS`. -/// -/// On construction, ensures that the `STANDALONE_PROCESS` is running. -/// -/// On drop, checks to see if it was the last `StandaloneHandle`, and if so, -/// terminates the `STANDALONE_PROCESS`. -pub struct StandaloneHandle { - _hidden: (), -} - -impl Default for StandaloneHandle { - fn default() -> Self { - let mut process = STANDALONE_PROCESS.lock().expect("STANDALONE_PROCESS Mutex is poisoned"); - if let Some(proc) = &mut *process { - proc.add_user() - .expect("Failed to add user for running spacetime standalone process"); - } else { - *process = Some(StandaloneProcess::start()); - } - StandaloneHandle { _hidden: () } - } -} - -impl Drop for StandaloneHandle { - fn drop(&mut self) { - let mut process = STANDALONE_PROCESS.lock().expect("STANDALONE_PROCESS Mutex is poisoned"); - if let Some(proc) = &mut *process { - if proc - .sub_user() - .expect("Failed to remove user for running spacetime standalone process") - { - *process = None; - } - } + if join_handle + .as_ref() + .expect("Standalone process already finished") + .is_finished() + { + join_handle.take().unwrap().join().expect("Standalone process failed"); } } @@ -180,7 +99,7 @@ impl Test { TestBuilder::default() } pub fn run(&self) { - let _handle = StandaloneHandle::default(); + ensure_standalone_process(); let compiled = CompiledModule::compile(&self.module_name); @@ -189,12 +108,11 @@ impl Test { compiled.path(), &self.client_project, &self.generate_subdir, - &self.name, ); compile_client(&self.compile_command, &self.client_project, &self.name); - let db_name = publish_module(&self.module_name, &self.name); + let db_name = publish_module(&self.module_name); run_client(&self.run_command, &self.client_project, &db_name, &self.name); } @@ -216,22 +134,20 @@ fn random_module_name() -> String { Alphanumeric.sample_string(&mut rand::thread_rng(), 16) } -fn publish_module(module: &str, test_name: &str) -> String { +fn publish_module(module: &str) -> String { let name = random_module_name(); - let output = cmd!("spacetime", "publish", "--skip_clippy", name.clone(),) - .stderr_to_stdout() - .stdout_capture() - .dir(module_path(module)) - .unchecked() - .run() - .expect("Error running spacetime publish"); - - status_ok_or_panic(output, "spacetime publish", test_name); + invoke_cli(&[ + "publish", + "--project-path", + module_path(module).to_str().unwrap(), + "--skip_clippy", + &name, + ]); name } -fn generate_bindings(language: &str, path: &Path, client_project: &str, generate_subdir: &str, test_name: &str) { +fn generate_bindings(language: &str, path: &Path, client_project: &str, generate_subdir: &str) { let generate_dir = format!("{}/{}", client_project, generate_subdir); let mut bindings_lock = BINDINGS_GENERATED.lock().expect("BINDINGS_GENERATED Mutex is poisoned"); @@ -245,24 +161,16 @@ fn generate_bindings(language: &str, path: &Path, client_project: &str, generate } create_dir_all(&generate_dir).expect("Error creating generate subdir"); - let output = cmd!( - "spacetime", + invoke_cli(&[ "generate", "--skip_clippy", "--lang", language, "--wasm-file", - path, + path.to_str().unwrap(), "--out-dir", - generate_dir - ) - .stderr_to_stdout() - .stdout_capture() - .unchecked() - .run() - .expect("Error running spacetime generate"); - - status_ok_or_panic(output, "spacetime generate", test_name); + &generate_dir, + ]); } fn split_command_string(command: &str) -> (&str, Vec<&str>) {