From 540c519002c7d6145fb585dfc19d5bb4db23b187 Mon Sep 17 00:00:00 2001 From: Noa Date: Wed, 13 Mar 2024 21:47:38 -0500 Subject: [PATCH] Rewrite smoketests as python unittests (#692) * Rewrite smoketests as python unittests * Get all tests working and do some work on parallel unittest * Give up on parallel unittests * Fix CI + address comments * Fix skip-clippy arg confusion (just use the env var) * fix ci * Add comments --- .github/workflows/ci.yml | 4 +- .gitignore | 3 + crates/cli/src/config.rs | 68 ++-- crates/cli/src/subcommands/logs.rs | 21 +- crates/cli/src/subcommands/publish.rs | 2 + crates/cli/src/tasks/rust.rs | 2 +- crates/client-api/src/routes/identity.rs | 23 +- crates/sats/src/de/serde.rs | 17 +- crates/standalone/src/control_db.rs | 2 +- smoketests/__init__.py | 214 ++++++++++ smoketests/__main__.py | 96 +++++ smoketests/config.toml | 6 + smoketests/tests/__init__.py | 0 smoketests/tests/autoinc.py | 117 ++++++ smoketests/tests/cancel_reducer.py | 33 ++ .../tests/connect_disconnect_from_cli.py | 35 ++ smoketests/tests/create_project.py | 18 + smoketests/tests/default_module_clippy.py | 12 + smoketests/tests/describe.py | 31 ++ smoketests/tests/detect_wasm_bindgen.py | 25 ++ smoketests/tests/domains.py | 50 +++ smoketests/tests/filtering.py | 250 ++++++++++++ smoketests/tests/identity.py | 137 +++++++ smoketests/tests/module_nested_op.py | 53 +++ smoketests/tests/modules.py | 162 ++++++++ smoketests/tests/namespaces.py | 35 ++ smoketests/tests/new_user_flow.py | 51 +++ smoketests/tests/panic.py | 32 ++ smoketests/tests/permissions.py | 105 +++++ smoketests/tests/servers.py | 25 ++ .../sql-format.sh => smoketests/tests/sql.py | 38 +- {test => smoketests}/tests/template | 0 smoketests/tests/zz_docker.py | 135 +++++++ smoketests/unittest_parallel.py | 376 ++++++++++++++++++ test/config.toml | 7 - test/lib.include | 119 ------ test/run-smoke-tests.sh | 329 --------------- test/tests/autoinc1.sh | 69 ---- test/tests/autoinc2.sh | 84 ---- test/tests/bitcraftmini-pretest.sh | 19 - test/tests/cancel-reducer.sh | 40 -- test/tests/connect-disconnect-from-cli.sh | 41 -- test/tests/create-project-1.sh | 16 - test/tests/default-module-clippy.sh | 13 - test/tests/describe.sh | 40 -- test/tests/detect-wasm-bindgen.sh | 31 -- test/tests/filtering.sh | 273 ------------- test/tests/identity-import-test.sh | 26 -- test/tests/identity-new-email.sh | 36 -- test/tests/identity-remove-1.sh | 27 -- test/tests/identity-remove-2.sh | 27 -- test/tests/identity-set-default.sh | 25 -- test/tests/identity-set-email.sh | 36 -- test/tests/module-nested-op.sh | 64 --- test/tests/namespace-custom-cs.sh | 29 -- test/tests/namespace-spacetimedb-cs.sh | 24 -- test/tests/new-user-flow.sh | 61 --- test/tests/panic.sh | 43 -- test/tests/permissions-call.sh | 27 -- test/tests/permissions-delete.sh | 19 - test/tests/permissions-describe.sh | 22 - test/tests/permissions-logs.sh | 26 -- test/tests/permissions-private-table.sh | 39 -- test/tests/permissions-publish.sh | 19 - test/tests/register-domain.sh | 25 -- test/tests/reverse-dns.sh | 23 -- test/tests/setname.sh | 23 -- test/tests/spacetime-server.sh | 27 -- test/tests/upload-module-1.sh | 46 --- test/tests/upload-module-2.sh | 37 -- test/tests/zz_docker-restart-module.sh | 54 --- .../zz_docker-restart-repeating-reducer.sh | 43 -- test/tests/zz_docker-restart-sql.sh | 57 --- 73 files changed, 2084 insertions(+), 2060 deletions(-) create mode 100644 smoketests/__init__.py create mode 100644 smoketests/__main__.py create mode 100644 smoketests/config.toml create mode 100644 smoketests/tests/__init__.py create mode 100644 smoketests/tests/autoinc.py create mode 100644 smoketests/tests/cancel_reducer.py create mode 100644 smoketests/tests/connect_disconnect_from_cli.py create mode 100644 smoketests/tests/create_project.py create mode 100644 smoketests/tests/default_module_clippy.py create mode 100644 smoketests/tests/describe.py create mode 100644 smoketests/tests/detect_wasm_bindgen.py create mode 100644 smoketests/tests/domains.py create mode 100644 smoketests/tests/filtering.py create mode 100644 smoketests/tests/identity.py create mode 100644 smoketests/tests/module_nested_op.py create mode 100644 smoketests/tests/modules.py create mode 100644 smoketests/tests/namespaces.py create mode 100644 smoketests/tests/new_user_flow.py create mode 100644 smoketests/tests/panic.py create mode 100644 smoketests/tests/permissions.py create mode 100644 smoketests/tests/servers.py rename test/tests/sql-format.sh => smoketests/tests/sql.py (58%) rename {test => smoketests}/tests/template (100%) create mode 100644 smoketests/tests/zz_docker.py create mode 100644 smoketests/unittest_parallel.py delete mode 100644 test/config.toml delete mode 100644 test/lib.include delete mode 100755 test/run-smoke-tests.sh delete mode 100644 test/tests/autoinc1.sh delete mode 100644 test/tests/autoinc2.sh delete mode 100644 test/tests/bitcraftmini-pretest.sh delete mode 100644 test/tests/cancel-reducer.sh delete mode 100644 test/tests/connect-disconnect-from-cli.sh delete mode 100644 test/tests/create-project-1.sh delete mode 100644 test/tests/default-module-clippy.sh delete mode 100644 test/tests/describe.sh delete mode 100644 test/tests/detect-wasm-bindgen.sh delete mode 100644 test/tests/filtering.sh delete mode 100644 test/tests/identity-import-test.sh delete mode 100644 test/tests/identity-new-email.sh delete mode 100644 test/tests/identity-remove-1.sh delete mode 100644 test/tests/identity-remove-2.sh delete mode 100644 test/tests/identity-set-default.sh delete mode 100644 test/tests/identity-set-email.sh delete mode 100644 test/tests/module-nested-op.sh delete mode 100644 test/tests/namespace-custom-cs.sh delete mode 100644 test/tests/namespace-spacetimedb-cs.sh delete mode 100644 test/tests/new-user-flow.sh delete mode 100644 test/tests/panic.sh delete mode 100644 test/tests/permissions-call.sh delete mode 100644 test/tests/permissions-delete.sh delete mode 100644 test/tests/permissions-describe.sh delete mode 100644 test/tests/permissions-logs.sh delete mode 100644 test/tests/permissions-private-table.sh delete mode 100644 test/tests/permissions-publish.sh delete mode 100644 test/tests/register-domain.sh delete mode 100644 test/tests/reverse-dns.sh delete mode 100644 test/tests/setname.sh delete mode 100644 test/tests/spacetime-server.sh delete mode 100644 test/tests/upload-module-1.sh delete mode 100644 test/tests/upload-module-2.sh delete mode 100644 test/tests/zz_docker-restart-module.sh delete mode 100644 test/tests/zz_docker-restart-repeating-reducer.sh delete mode 100644 test/tests/zz_docker-restart-sql.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dfc703717..b35b1227ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,11 @@ jobs: - name: Start containers run: docker compose up -d - name: Run smoketests - run: test/run-smoke-tests.sh --parallel -x bitcraftmini-pretest zz_docker-restart-module zz_docker-restart-repeating-reducer zz_docker-restart-sql + run: python -m smoketests --docker -x zz_docker # These cannot run in parallel, even though the script tries to handle it # TODO: Fix flaky tests https://github.com/clockworklabs/SpacetimeDB/issues/630 # - name: Run restarting smoketests - # run: test/run-smoke-tests.sh zz_docker-restart-module zz_docker-restart-repeating-reducer zz_docker-restart-sql + # run: python -m smoketests --docker zz_docker - name: Stop containers if: always() run: docker compose down diff --git a/.gitignore b/.gitignore index fd77d63d77..2dd1541c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -192,6 +192,9 @@ linux-target/ /target #Cargo.lock +__pycache__/ +.test_out + ## JetBrains .idea/ diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 8dc57fa291..b2fd38cce3 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -6,7 +6,6 @@ use spacetimedb::auth::identity::decode_token; use spacetimedb_lib::Identity; use std::{ fs, - io::{Read, Write}, path::{Path, PathBuf}, }; @@ -789,34 +788,27 @@ impl Config { &self.home.server_configs } - fn find_config_filename(config_dir: &PathBuf) -> Option<&'static str> { - let read_dir = fs::read_dir(config_dir).unwrap(); - let filenames = [DOT_SPACETIME_FILENAME, SPACETIME_FILENAME, CONFIG_FILENAME]; - let mut config_filename = None; - 'outer: for path in read_dir { - for name in filenames { - if name == path.as_ref().unwrap().file_name().to_str().unwrap() { - config_filename = Some(name); - break 'outer; - } - } - } - config_filename + fn find_config_path(config_dir: &Path) -> Option { + [DOT_SPACETIME_FILENAME, SPACETIME_FILENAME, CONFIG_FILENAME] + .iter() + .map(|filename| config_dir.join(filename)) + .find(|path| path.exists()) } fn load_raw(config_dir: PathBuf, is_project: bool) -> RawConfig { // If a config file overload has been specified, use that instead if !is_project { if let Some(config_path) = std::env::var_os("SPACETIME_CONFIG_FILE") { - return Self::load_from_file(config_path.as_ref()); + return Self::load_from_file(config_path.as_ref()) + .inspect_err(|e| eprintln!("SPACETIME_CONFIG_FILE does not point to a valid config file: {e:#}")) + .unwrap_or_default(); } } if !config_dir.exists() { fs::create_dir_all(&config_dir).unwrap(); } - let config_filename = Self::find_config_filename(&config_dir); - let Some(config_filename) = config_filename else { + let Some(config_path) = Self::find_config_path(&config_dir) else { return if is_project { // Return an empty config without creating a file. RawConfig::default() @@ -828,21 +820,14 @@ impl Config { }; }; - let config_path = config_dir.join(config_filename); Self::load_from_file(&config_path) + .inspect_err(|e| eprintln!("config file is invalid: {e:#}")) + .unwrap_or_default() } - fn load_from_file(config_path: &Path) -> RawConfig { - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .read(true) - .open(config_path) - .unwrap(); - - let mut text = String::new(); - file.read_to_string(&mut text).unwrap(); - toml::from_str(&text).unwrap() + fn load_from_file(config_path: &Path) -> anyhow::Result { + let text = fs::read_to_string(config_path)?; + Ok(toml::from_str(&text)?) } pub fn load() -> Self { @@ -870,8 +855,10 @@ impl Config { } pub fn save(&self) { - let config_path = if let Some(config_path) = std::env::var_os("SPACETIME_CONFIG_FILE") { - PathBuf::from(&config_path) + let config_var = std::env::var_os("SPACETIME_CONFIG_FILE"); + let config_var_exists = config_var.is_some(); + let config_path = if let Some(config_path) = config_var { + PathBuf::from(config_path) } else { let home_dir = dirs::home_dir().unwrap(); let config_dir = home_dir.join(HOME_CONFIG_DIR); @@ -879,21 +866,16 @@ impl Config { fs::create_dir_all(&config_dir).unwrap(); } - let config_filename = Self::find_config_filename(&config_dir).unwrap_or(CONFIG_FILENAME); - config_dir.join(config_filename) + Self::find_config_path(&config_dir).unwrap_or_else(|| config_dir.join(CONFIG_FILENAME)) }; - let mut file = fs::OpenOptions::new() - .create(true) - .write(true) - .open(config_path) - .unwrap(); + let config = toml::to_string_pretty(&self.home).unwrap(); - let str = toml::to_string_pretty(&self.home).unwrap(); - - file.set_len(0).unwrap(); - file.write_all(str.as_bytes()).unwrap(); - file.sync_all().unwrap(); + if let Err(e) = std::fs::write(config_path, config) { + if !config_var_exists { + eprintln!("could not save config file: {e}") + } + } } pub fn get_default_identity_config(&self, server: Option<&str>) -> anyhow::Result<&IdentityConfig> { diff --git a/crates/cli/src/subcommands/logs.rs b/crates/cli/src/subcommands/logs.rs index 0a5353d08c..a617990243 100644 --- a/crates/cli/src/subcommands/logs.rs +++ b/crates/cli/src/subcommands/logs.rs @@ -7,6 +7,7 @@ use clap::{Arg, ArgAction, ArgMatches}; use futures::{AsyncBufReadExt, TryStreamExt}; use is_terminal::IsTerminal; use termcolor::{Color, ColorSpec, WriteColor}; +use tokio::io::AsyncWriteExt; pub fn cli() -> clap::Command { clap::Command::new("logs") @@ -43,6 +44,13 @@ pub fn cli() -> clap::Command { .help("A flag indicating whether or not to follow the logs") .long_help("A flag that causes logs to not stop when end of the log file is reached, but rather to wait for additional data to be appended to the input."), ) + .arg( + Arg::new("json") + .long("json") + .required(false) + .action(ArgAction::SetTrue) + .help("Output raw json log records"), + ) .after_help("Run `spacetime help logs` for more detailed information.\n") } @@ -93,6 +101,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let num_lines = args.get_one::("num_lines").copied(); let database = args.get_one::("database").unwrap(); let follow = args.get_flag("follow"); + let json = args.get_flag("json"); let auth_header = get_auth_header_only(&mut config, false, identity, server).await?; @@ -103,7 +112,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", config.get_host_url(server)?, address)); let builder = add_auth_header_opt(builder, &auth_header); - let res = builder.query(&query_parms).send().await?; + let mut res = builder.query(&query_parms).send().await?; let status = res.status(); if status.is_client_error() || status.is_server_error() { @@ -111,7 +120,15 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E anyhow::bail!(err) } - let term_color = if std::io::stderr().is_terminal() { + if json { + let mut stdout = tokio::io::stdout(); + while let Some(chunk) = res.chunk().await? { + stdout.write_all(&chunk).await?; + } + return Ok(()); + } + + let term_color = if std::io::stdout().is_terminal() { termcolor::ColorChoice::Auto } else { termcolor::ColorChoice::Never diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index 2b332c7117..46405fd874 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -149,6 +149,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E database_host ); + eprintln!("Publishing module..."); + let mut builder = reqwest::Client::new().post(Url::parse_with_params( format!("{}/database/publish", database_host).as_str(), query_params, diff --git a/crates/cli/src/tasks/rust.rs b/crates/cli/src/tasks/rust.rs index 6c55e2880a..cc485ac4d9 100644 --- a/crates/cli/src/tasks/rust.rs +++ b/crates/cli/src/tasks/rust.rs @@ -32,7 +32,7 @@ pub(crate) fn build_rust(project_path: &Path, skip_clippy: bool, build_debug: bo if !skip_clippy { let clippy_conf_dir = tempfile::tempdir()?; fs::write(clippy_conf_dir.path().join("clippy.toml"), CLIPPY_TOML)?; - println!("checking crate with spacetimedb's clippy configuration"); + eprintln!("checking crate with spacetimedb's clippy configuration"); let out = cargo_cmd( "clippy", build_debug, diff --git a/crates/client-api/src/routes/identity.rs b/crates/client-api/src/routes/identity.rs index 71269682e4..4be4a2ff8f 100644 --- a/crates/client-api/src/routes/identity.rs +++ b/crates/client-api/src/routes/identity.rs @@ -63,21 +63,14 @@ pub async fn get_identity( None => None, Some(email) => { let identities = ctx.get_identities_for_email(email.as_str()).map_err(log_and_500)?; - if identities.is_empty() { - None - } else { - let mut response = GetIdentityResponse { - identities: Vec::::new(), - }; - - for identity_email in identities { - response.identities.push(GetIdentityResponseEntry { - identity: identity_email.identity, - email: identity_email.email, - }) - } - Some(response) - } + let identities = identities + .into_iter() + .map(|identity_email| GetIdentityResponseEntry { + identity: identity_email.identity, + email: identity_email.email, + }) + .collect::>(); + (!identities.is_empty()).then_some(GetIdentityResponse { identities }) } }; let identity_response = lookup.ok_or(StatusCode::NOT_FOUND)?; diff --git a/crates/sats/src/de/serde.rs b/crates/sats/src/de/serde.rs index 4dc8bc4dca..393a40f6be 100644 --- a/crates/sats/src/de/serde.rs +++ b/crates/sats/src/de/serde.rs @@ -107,10 +107,11 @@ impl<'de, D: serde::Deserializer<'de>> Deserializer<'de> for SerdeDeserializer>(self, visitor: V) -> Result { if self.de.is_human_readable() { - self.de.deserialize_any(BytesVisitor { visitor }).map_err(SerdeError) + self.de.deserialize_any(BytesVisitor::<_, true> { visitor }) } else { - self.de.deserialize_bytes(BytesVisitor { visitor }).map_err(SerdeError) + self.de.deserialize_bytes(BytesVisitor::<_, false> { visitor }) } + .map_err(SerdeError) } fn deserialize_array_seed, T: super::DeserializeSeed<'de> + Clone>( @@ -466,16 +467,22 @@ impl<'de, V: super::SliceVisitor<'de, str>> serde::Visitor<'de> for StrVisitor` to `serde::Visitor<'de>` /// for implementing `deserialize_bytes`. -struct BytesVisitor { +struct BytesVisitor { /// The `SliceVisitor<'de, [u8]>`. visitor: V, } -impl<'de, V: super::SliceVisitor<'de, [u8]>> serde::Visitor<'de> for BytesVisitor { +impl<'de, V: super::SliceVisitor<'de, [u8]>, const HUMAN_READABLE: bool> serde::Visitor<'de> + for BytesVisitor +{ type Value = V::Output; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("a byte array") + f.write_str(if HUMAN_READABLE { + "a byte array or hex string" + } else { + "a byte array" + }) } fn visit_bytes(self, v: &[u8]) -> Result { diff --git a/crates/standalone/src/control_db.rs b/crates/standalone/src/control_db.rs index c146c0241d..44837b98f5 100644 --- a/crates/standalone/src/control_db.rs +++ b/crates/standalone/src/control_db.rs @@ -282,7 +282,7 @@ impl ControlDb { for i in tree.iter() { let (_, value) = i?; let iemail: IdentityEmail = bsatn::from_slice(&value)?; - if iemail.email == email { + if iemail.email.eq_ignore_ascii_case(email) { result.push(iemail); } } diff --git a/smoketests/__init__.py b/smoketests/__init__.py new file mode 100644 index 0000000000..e8fe562374 --- /dev/null +++ b/smoketests/__init__.py @@ -0,0 +1,214 @@ +from pathlib import Path +import contextlib +import json +import os +import random +import re +import shutil +import string +import string +import subprocess +import sys +import tempfile +import unittest + +# miscellaneous file paths +TEST_DIR = Path(__file__).parent +STDB_DIR = TEST_DIR.parent +SPACETIME_BIN = STDB_DIR / "target/debug/spacetime" +TEMPLATE_TARGET_DIR = STDB_DIR / "target/_stdbsmoketests" +STDB_CONFIG = TEST_DIR / "config.toml" + +# the contents of files for the base smoketest project template +TEMPLATE_LIB_RS = open(STDB_DIR / "crates/cli/src/subcommands/project/rust/lib._rs").read() +TEMPLATE_CARGO_TOML = open(STDB_DIR / "crates/cli/src/subcommands/project/rust/Cargo._toml").read() +bindings_path = (STDB_DIR / "crates/bindings").absolute() +TEMPLATE_CARGO_TOML = (re.compile(r"^spacetimedb\s*=.*$", re.M) \ + .sub(f'spacetimedb = {{ path = "{bindings_path}" }}', TEMPLATE_CARGO_TOML)) + +# this is set to true when the --docker flag is passed to the cli +HAVE_DOCKER = False + + +def build_template_target(): + if not TEMPLATE_TARGET_DIR.exists(): + print("Building base compilation artifacts") + class BuildModule(Smoketest): + AUTOPUBLISH = False + + BuildModule.setUpClass() + env = { **os.environ, "CARGO_TARGET_DIR": TEMPLATE_TARGET_DIR } + spacetime("build", BuildModule.project_path, env=env, capture_stderr=False) + BuildModule.tearDownClass() + BuildModule.doClassCleanups() + + +def requires_docker(item): + if HAVE_DOCKER: + return item + return unittest.skip("docker not available")(item) + +def random_string(k=20): + return ''.join(random.choices(string.ascii_letters, k=k)) + +def extract_fields(cmd_output, field_name): + """ + parses output from the spacetime cli that's formatted in the "empty" style + from tabled: + FIELDNAME1 VALUE1 + THEFIELDNAME2 VALUE2 + field_name should be which field name you want to filter for + """ + out = [] + for line in cmd_output.splitlines(): + fields = line.split() + if len(fields) < 2: + continue + label, val, *_ = fields + if label == field_name: + out.append(val) + return out + +def extract_field(cmd_output, field_name): + field, = extract_fields(cmd_output, field_name) + return field + +def run_cmd(*args, capture_stderr=True, check=True, full_output=False, cmd_name=None, **kwargs): + output = subprocess.run( + list(args), + encoding="utf8", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE if capture_stderr else None, + **kwargs + ) + if capture_stderr: + sys.stderr.write(output.stderr) + sys.stderr.flush() + if check: + if cmd_name is not None: + output.args[0] = "spacetime" + output.check_returncode() + return output if full_output else output.stdout + + +def spacetime(*args, **kwargs): + return run_cmd(SPACETIME_BIN, *args, cmd_name="spacetime", **kwargs) + +class Smoketest(unittest.TestCase): + MODULE_CODE = TEMPLATE_LIB_RS + AUTOPUBLISH = True + EXTRA_DEPS = "" + + @classmethod + def cargo_manifest(cls, manifest_text): + return manifest_text + cls.EXTRA_DEPS + + # helpers + + @classmethod + def spacetime(cls, *args, **kwargs): + kwargs.setdefault("env", os.environ.copy())["SPACETIME_CONFIG_FILE"] = str(cls.config_path) + return spacetime(*args, **kwargs) + + def _check_published(self): + if not hasattr(self, "address"): + raise Exception("Cannot use this function without publishing a module") + + def call(self, reducer, *args): + self._check_published() + self.spacetime("call", "--", self.address, reducer, *map(json.dumps, args)) + + def logs(self, n): + return [log["message"] for log in self.log_records(n)] + + def log_records(self, n): + self._check_published() + logs = self.spacetime("logs", "--json", "--", self.address, str(n)) + return list(map(json.loads, logs.splitlines())) + + def publish_module(self, domain=None, *, clear=True, capture_stderr=True): + publish_output = self.spacetime( + "publish", + *[domain] if domain is not None else [], + "--project-path", self.project_path, + *(["-c"] if clear else []), + capture_stderr=capture_stderr, + ) + self.resolved_address = re.search(r"address: ([0-9a-fA-F]+)", publish_output)[1] + self.address = domain if domain is not None else self.resolved_address + + @classmethod + def reset_config(cls): + shutil.copy(STDB_CONFIG, cls.config_path) + + def fingerprint(self): + # Fetch the server's fingerprint; required for `identity list`. + self.spacetime("server", "fingerprint", "localhost", "-f") + + def new_identity(self, *, email, default=False): + output = self.spacetime("identity", "new", "--no-email" if email is None else f"--email={email}") + identity = extract_field(output, "IDENTITY") + if default: + self.spacetime("identity", "set-default", identity) + return identity + + def token(self, identity): + return self.spacetime("identity", "token", identity).strip() + + def import_identity(self, identity, token, *, default=False): + self.spacetime("identity", "import", identity, token) + if default: + self.spacetime("identity", "set-default", identity) + + @classmethod + def write_module_code(cls, module_code): + open(cls.project_path / "src/lib.rs", "w").write(module_code) + + # testcase initialization + + + @classmethod + def setUpClass(cls): + cls.project_path = Path(cls.enterClassContext(tempfile.TemporaryDirectory())) + cls.config_path = cls.project_path / "config.toml" + cls.reset_config() + open(cls.project_path / "Cargo.toml", "w").write(cls.cargo_manifest(TEMPLATE_CARGO_TOML)) + shutil.copy2(STDB_DIR / "rust-toolchain.toml", cls.project_path) + os.mkdir(cls.project_path / "src") + cls.write_module_code(cls.MODULE_CODE) + if TEMPLATE_TARGET_DIR.exists(): + shutil.copytree(TEMPLATE_TARGET_DIR, cls.project_path / "target") + + if cls.AUTOPUBLISH: + print(f"Compiling module for {cls.__qualname__}...", file=sys.__stderr__) + cls.publish_module(cls, capture_stderr=False) + + def tearDown(self): + # if this single test method published a database, clean it up now + if "address" in self.__dict__: + try: + # TODO: save the credentials in publish_module() + self.spacetime("delete", self.address, capture_stderr=False) + except Exception: + pass + + @classmethod + def tearDownClass(cls): + if hasattr(cls, "address"): + try: + # TODO: save the credentials in publish_module() + cls.spacetime("delete", cls.address, capture_stderr=False) + except Exception: + pass + + # def setUp(self): + # if self.AUTOPUBLISH: + # self.spacetime("publish", "-S", "--project-path", self.project_path) + + if sys.version_info < (3, 11): + # polyfill; python 3.11 defines this classmethod on TestCase + @classmethod + def enterClassContext(cls, cm): + result = cm.__enter__() + cls.addClassCleanup(cm.__exit__, None, None, None) + return result diff --git a/smoketests/__main__.py b/smoketests/__main__.py new file mode 100644 index 0000000000..a303d028bd --- /dev/null +++ b/smoketests/__main__.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +import subprocess +import unittest +import argparse +from subprocess import check_call, check_output +from tempfile import TemporaryDirectory +from pathlib import Path +import os +import re +import fnmatch +import json +from . import TEST_DIR, STDB_DIR, STDB_CONFIG, build_template_target +import smoketests + + +def check_docker(): + docker_ps = check_output(["docker", "ps", "--format=json"]) + docker_ps = (json.loads(line) for line in docker_ps.splitlines()) + for docker_container in docker_ps: + if "node" in docker_container["Image"]: + return docker_container["Names"] + else: + print("Docker container not found, is SpacetimeDB running?") + exit(1) + +class ExclusionaryTestLoader(unittest.TestLoader): + def __init__(self, excludelist=()): + super().__init__() + # build a regex that matches any of the elements of excludelist at a word boundary + excludes = '|'.join(fnmatch.translate(exclude).removesuffix(r"\Z") for exclude in excludelist) + self.excludepat = excludes and re.compile(rf"^(?:{excludes})\b") + + def loadTestsFromName(self, name, module=None): + if self.excludepat: + qualname = name + if module is not None: + qualname = module.__name__ + "." + name + if self.excludepat.match(qualname): + return self.suiteClass([]) + return super().loadTestsFromName(name, module) + +def _convert_select_pattern(pattern): + return f'*{pattern}*' if '*' not in pattern else pattern + +TESTPREFIX = "smoketests.tests." +def main(): + tests = [fname.removesuffix(".py") for fname in os.listdir(TEST_DIR / "tests") if fname.endswith(".py") and fname != "__init__.py"] + + parser = argparse.ArgumentParser() + parser.add_argument("test", nargs="*", default=tests) + parser.add_argument("--docker", action="store_true") + parser.add_argument("--show-all-output", action="store_true", help="show all stdout/stderr from the tests as they're running") + parser.add_argument("--parallel", action="store_true", help="run test classes in parallel") + parser.add_argument("-j", dest='jobs', help="Set number of jobs for parallel test runs. Default is `nproc`", type=int, default=0) + parser.add_argument('-k', dest='testNamePatterns', + action='append', type=_convert_select_pattern, + help='Only run tests which match the given substring') + parser.add_argument("-x", dest="exclude", nargs="*", default=[]) + args = parser.parse_args() + + print("Compiling spacetime cli...") + check_call(["cargo", "build"], cwd=TEST_DIR.parent) + + os.environ["SPACETIME_SKIP_CLIPPY"] = "1" + + build_template_target() + + if args.docker: + docker_container = check_docker() + # have docker logs print concurrently with the test output + subprocess.Popen(["docker", "logs", "-f", docker_container]) + smoketests.HAVE_DOCKER = True + + add_prefix = lambda testlist: [TESTPREFIX + test for test in testlist] + import fnmatch + excludelist = add_prefix(args.exclude) + testlist = add_prefix(args.test) + + loader = ExclusionaryTestLoader(excludelist) + loader.testNamePatterns = args.testNamePatterns + + tests = loader.loadTestsFromNames(testlist) + buffer = not args.show_all_output + verbosity = 2 + + if args.parallel: + print("parallel test running is under construction, this will probably not work correctly") + from . import unittest_parallel + unittest_parallel.main(buffer=buffer, verbose=verbosity, level="class", discovered_tests=tests, jobs=args.jobs) + else: + result = unittest.TextTestRunner(buffer=buffer, verbosity=verbosity).run(tests) + + +if __name__ == '__main__': + main() diff --git a/smoketests/config.toml b/smoketests/config.toml new file mode 100644 index 0000000000..5a37a8381b --- /dev/null +++ b/smoketests/config.toml @@ -0,0 +1,6 @@ +default_server = "127.0.0.1:3000" + +[[server_configs]] +nickname = "localhost" +host = "127.0.0.1:3000" +protocol = "http" diff --git a/smoketests/tests/__init__.py b/smoketests/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/smoketests/tests/autoinc.py b/smoketests/tests/autoinc.py new file mode 100644 index 0000000000..6ac12aed75 --- /dev/null +++ b/smoketests/tests/autoinc.py @@ -0,0 +1,117 @@ +from .. import Smoketest +import string +import functools + + +ints = "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128" + +class IntTests: + make_func = lambda int_ty: lambda self: self.do_test_autoinc(int_ty) + for int_ty in ints: + locals()[f"test_autoinc_{int_ty}"] = make_func(int_ty) + del int_ty, make_func + + + +autoinc1_template = string.Template(""" +#[spacetimedb(table)] +pub struct Person_$KEY_TY { + #[autoinc] + key_col: $KEY_TY, + name: String, +} + +#[spacetimedb(reducer)] +pub fn add_$KEY_TY(name: String, expected_value: $KEY_TY) { + let value = Person_$KEY_TY::insert(Person_$KEY_TY { key_col: 0, name }); + assert_eq!(value.key_col, expected_value); +} + +#[spacetimedb(reducer)] +pub fn say_hello_$KEY_TY() { + for person in Person_$KEY_TY::iter() { + println!("Hello, {}:{}!", person.key_col, person.name); + } + println!("Hello, World!"); +} +""") + + + +class AutoincBasic(IntTests, Smoketest): + "This tests the autoinc functionality" + + MODULE_CODE = f""" +#![allow(non_camel_case_types)] +use spacetimedb::{{println, spacetimedb}}; +{"".join(autoinc1_template.substitute(KEY_TY=int_ty) for int_ty in ints)} +""" + + def do_test_autoinc(self, int_ty): + self.call(f"add_{int_ty}", "Robert", 1) + self.call(f"add_{int_ty}", "Julie", 2) + self.call(f"add_{int_ty}", "Samantha", 3) + self.call(f"say_hello_{int_ty}") + logs = self.logs(4) + self.assertIn("Hello, 3:Samantha!", logs) + self.assertIn("Hello, 2:Julie!", logs) + self.assertIn("Hello, 1:Robert!", logs) + self.assertIn("Hello, World!", logs) + + + +autoinc2_template = string.Template(""" +#[spacetimedb(table)] +pub struct Person_$KEY_TY { + #[autoinc] + #[unique] + key_col: $KEY_TY, + #[unique] + name: String, +} + +#[spacetimedb(reducer)] +pub fn add_new_$KEY_TY(name: String) -> Result<(), Box> { + let value = Person_$KEY_TY::insert(Person_$KEY_TY { key_col: 0, name })?; + println!("Assigned Value: {} -> {}", value.key_col, value.name); + Ok(()) +} + +#[spacetimedb(reducer)] +pub fn update_$KEY_TY(name: String, new_id: $KEY_TY) { + Person_$KEY_TY::delete_by_name(&name); + let _value = Person_$KEY_TY::insert(Person_$KEY_TY { key_col: new_id, name }); +} + +#[spacetimedb(reducer)] +pub fn say_hello_$KEY_TY() { + for person in Person_$KEY_TY::iter() { + println!("Hello, {}:{}!", person.key_col, person.name); + } + println!("Hello, World!"); +} +""") + + +class AutoincUnique(IntTests, Smoketest): + """This tests unique constraints being violated during autoinc insertion""" + + MODULE_CODE = f""" +#![allow(non_camel_case_types)] +use std::error::Error; +use spacetimedb::{{println, spacetimedb}}; +{"".join(autoinc2_template.substitute(KEY_TY=int_ty) for int_ty in ints)} +""" + + def do_test_autoinc(self, int_ty): + self.call(f"update_{int_ty}", "Robert", 2) + self.call(f"add_new_{int_ty}", "Success") + with self.assertRaises(Exception): + self.call(f"add_new_{int_ty}", "Failure") + + self.call(f"say_hello_{int_ty}") + logs = self.logs(4) + self.assertIn("Hello, 2:Robert!", logs) + self.assertIn("Hello, 1:Success!", logs) + self.assertIn("Hello, World!", logs) + diff --git a/smoketests/tests/cancel_reducer.py b/smoketests/tests/cancel_reducer.py new file mode 100644 index 0000000000..31a2daeaa2 --- /dev/null +++ b/smoketests/tests/cancel_reducer.py @@ -0,0 +1,33 @@ +from .. import Smoketest +import time + +class CancelReducer(Smoketest): + + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb, ScheduleToken}; + +#[spacetimedb(init)] +fn init() { + let token = spacetimedb::schedule!("100ms", reducer(1)); + token.cancel(); + let token = spacetimedb::schedule!("1000ms", reducer(2)); + spacetimedb::schedule!("500ms", do_cancel(token)); +} + +#[spacetimedb(reducer)] +fn do_cancel(token: ScheduleToken) { + token.cancel() +} + +#[spacetimedb(reducer)] +fn reducer(num: i32) { + println!("the reducer ran: {}", num) +} +""" + + def test_cancel_reducer(self): + """Ensure cancelling a reducer works""" + + time.sleep(2) + logs = "\n".join(self.logs(5)) + self.assertNotIn("the reducer ran", logs) diff --git a/smoketests/tests/connect_disconnect_from_cli.py b/smoketests/tests/connect_disconnect_from_cli.py new file mode 100644 index 0000000000..9a7cd66da2 --- /dev/null +++ b/smoketests/tests/connect_disconnect_from_cli.py @@ -0,0 +1,35 @@ +from .. import Smoketest + +class ConnDisconnFromCli(Smoketest): + + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb, ReducerContext}; + +#[spacetimedb(connect)] +pub fn connected(_ctx: ReducerContext) { + println!("_connect called"); + panic!("Panic on connect"); +} + +#[spacetimedb(disconnect)] +pub fn disconnected(_ctx: ReducerContext) { + println!("disconnect called"); + panic!("Panic on disconnect"); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + println!("Hello, World!"); +} +""" + + def test_conn_disconn(self): + """ + Ensure that the connect and disconnect functions are called when invoking a reducer from the CLI + """ + + self.call("say_hello") + logs = self.logs(10) + self.assertIn('_connect called', logs) + self.assertIn('disconnect called', logs) + self.assertIn('Hello, World!', logs) diff --git a/smoketests/tests/create_project.py b/smoketests/tests/create_project.py new file mode 100644 index 0000000000..cb554e8eed --- /dev/null +++ b/smoketests/tests/create_project.py @@ -0,0 +1,18 @@ +from .. import spacetime +import unittest +import tempfile + +class CreateProject(unittest.TestCase): + def test_create_project(self): + """ + Ensure that the CLI is able to create a local project. This test does not depend on a running spacetimedb instance. + """ + + with tempfile.TemporaryDirectory() as tmpdir: + with self.assertRaises(Exception): + spacetime("init") + with self.assertRaises(Exception): + spacetime("init", tmpdir) + spacetime("init", "--lang=rust", tmpdir) + with self.assertRaises(Exception): + spacetime("init", "--lang=rust", tmpdir) \ No newline at end of file diff --git a/smoketests/tests/default_module_clippy.py b/smoketests/tests/default_module_clippy.py new file mode 100644 index 0000000000..954e6a47f7 --- /dev/null +++ b/smoketests/tests/default_module_clippy.py @@ -0,0 +1,12 @@ +import unittest +import tempfile +import subprocess +from .. import Smoketest + +class ClippyDefaultModule(Smoketest): + AUTOPUBLISH = False + + def test_default_module_clippy_check(self): + """Ensure that the default rust module has no clippy errors or warnings""" + + subprocess.check_call(["cargo", "clippy", "--", "-Dwarnings"], cwd=self.project_path) \ No newline at end of file diff --git a/smoketests/tests/describe.py b/smoketests/tests/describe.py new file mode 100644 index 0000000000..39a0b4e3cb --- /dev/null +++ b/smoketests/tests/describe.py @@ -0,0 +1,31 @@ +from .. import Smoketest + +class ModuleDescription(Smoketest): + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +pub struct Person { + name: String, +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { + Person::insert(Person { name }); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + + def test_describe(self): + """Check describing a module""" + + self.spacetime("describe", self.address) + self.spacetime("describe", self.address, "reducer", "say_hello") + self.spacetime("describe", self.address, "table", "Person") diff --git a/smoketests/tests/detect_wasm_bindgen.py b/smoketests/tests/detect_wasm_bindgen.py new file mode 100644 index 0000000000..b65e2edf70 --- /dev/null +++ b/smoketests/tests/detect_wasm_bindgen.py @@ -0,0 +1,25 @@ +from .. import Smoketest + +class WasmBindgen(Smoketest): + AUTOPUBLISH = False + MODULE_CODE = """ +use spacetimedb::{log, spacetimedb}; + +#[spacetimedb(reducer)] +pub fn test() { + log::info!("Hello! {}", now()); +} + +#[wasm_bindgen::prelude::wasm_bindgen] +extern "C" { + fn now() -> i32; +} +""" + EXTRA_DEPS = 'wasm-bindgen = "0.2"' + + def test_detect_wasm_bindgen(self): + """Ensure that spacetime build properly catches wasm_bindgen imports""" + + output = self.spacetime("build", self.project_path, full_output=True, check=False) + self.assertTrue(output.returncode) + self.assertIn("wasm-bindgen detected", output.stderr) diff --git a/smoketests/tests/domains.py b/smoketests/tests/domains.py new file mode 100644 index 0000000000..31e067e55c --- /dev/null +++ b/smoketests/tests/domains.py @@ -0,0 +1,50 @@ +from .. import Smoketest, random_string + +class Domains(Smoketest): + AUTOPUBLISH = False + + def test_register_domain(self): + """Attempts to register some valid domains and makes sure invalid domains cannot be registered""" + + rand_domain = random_string() + + identity = self.new_identity(email=None) + self.spacetime("dns", "register-tld", rand_domain) + + self.publish_module(rand_domain) + self.publish_module(f"{rand_domain}/test") + self.publish_module(f"{rand_domain}/test/test2") + + with self.assertRaises(Exception): + self.publish_module(f"{rand_domain}//test") + with self.assertRaises(Exception): + self.publish_module(f"{rand_domain}/test/") + with self.assertRaises(Exception): + self.publish_module(f"{rand_domain}/test//test2") + + def test_reverse_dns(self): + """This tests the functionality of spacetime reverse dns lookups""" + + rand_domain = random_string() + self.spacetime("dns", "register-tld", rand_domain) + + self.publish_module(rand_domain) + + names = self.spacetime("dns", "reverse-lookup", self.resolved_address).splitlines() + self.assertIn(rand_domain, names) + + def test_set_name(self): + """Tests the functionality of the set-name command""" + + self.spacetime("identity", "init-default") + self.publish_module() + + rand_name = random_string() + + self.spacetime("dns", "register-tld", rand_name) + self.spacetime("dns", "set-name", rand_name, self.address) + lookup_result = self.spacetime("dns", "lookup", rand_name).strip() + self.assertEqual(lookup_result, self.address) + + names = self.spacetime("dns", "reverse-lookup", self.address).splitlines() + self.assertIn(rand_name, names) \ No newline at end of file diff --git a/smoketests/tests/filtering.py b/smoketests/tests/filtering.py new file mode 100644 index 0000000000..21b87ece02 --- /dev/null +++ b/smoketests/tests/filtering.py @@ -0,0 +1,250 @@ +from .. import Smoketest + +class Filtering(Smoketest): + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb, Identity}; + +#[spacetimedb(table)] +pub struct Person { + #[unique] + id: i32, + + name: String, + #[unique] + nick: String, +} + +#[spacetimedb(reducer)] +pub fn insert_person(id: i32, name: String, nick: String) { + Person::insert(Person { id, name, nick} ); +} + +#[spacetimedb(reducer)] +pub fn insert_person_twice(id: i32, name: String, nick: String) { + Person::insert(Person { id, name: name.clone(), nick: nick.clone()} ); + match Person::insert(Person { id, name: name.clone(), nick: nick.clone()}) { + Ok(_) => {}, + Err(_) => { + println!("UNIQUE CONSTRAINT VIOLATION ERROR: id {}: {}", id, name) + } + } +} + +#[spacetimedb(reducer)] +pub fn delete_person(id: i32) { + Person::delete_by_id(&id); +} + +#[spacetimedb(reducer)] +pub fn find_person(id: i32) { + match Person::filter_by_id(&id) { + Some(person) => println!("UNIQUE FOUND: id {}: {}", id, person.name), + None => println!("UNIQUE NOT FOUND: id {}", id), + } +} + +#[spacetimedb(reducer)] +pub fn find_person_by_name(name: String) { + for person in Person::filter_by_name(&name) { + println!("UNIQUE FOUND: id {}: {} aka {}", person.id, person.name, person.nick); + } +} + +#[spacetimedb(reducer)] +pub fn find_person_by_nick(nick: String) { + match Person::filter_by_nick(&nick) { + Some(person) => println!("UNIQUE FOUND: id {}: {}", person.id, person.nick), + None => println!("UNIQUE NOT FOUND: nick {}", nick), + } +} + +#[spacetimedb(table)] +#[spacetimedb(index(btree, name = "by_id", id))] +pub struct NonuniquePerson { + id: i32, + name: String, + is_human: bool, +} + +#[spacetimedb(reducer)] +pub fn insert_nonunique_person(id: i32, name: String, is_human: bool) { + NonuniquePerson::insert(NonuniquePerson { id, name, is_human } ); +} + +#[spacetimedb(reducer)] +pub fn find_nonunique_person(id: i32) { + for person in NonuniquePerson::filter_by_id(&id) { + println!("NONUNIQUE FOUND: id {}: {}", id, person.name) + } +} + +#[spacetimedb(reducer)] +pub fn find_nonunique_humans() { + for person in NonuniquePerson::filter_by_is_human(&true) { + println!("HUMAN FOUND: id {}: {}", person.id, person.name); + } +} + +#[spacetimedb(reducer)] +pub fn find_nonunique_non_humans() { + for person in NonuniquePerson::filter_by_is_human(&false) { + println!("NON-HUMAN FOUND: id {}: {}", person.id, person.name); + } +} + +// Ensure that [Identity] is filterable and a legal unique column. +#[spacetimedb(table)] +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(id_number: u64, name: String) { + let identity = identify(id_number); + IdentifiedPerson::insert(IdentifiedPerson { identity, name }); +} + +#[spacetimedb(reducer)] +fn find_identified_person(id_number: u64) { + let identity = identify(id_number); + match IdentifiedPerson::filter_by_identity(&identity) { + Some(person) => println!("IDENTIFIED FOUND: {}", person.name), + None => println!("IDENTIFIED NOT FOUND"), + } +} + +// Ensure that indices on non-unique columns behave as we expect. +#[spacetimedb(table)] +#[spacetimedb(index(btree, name="person_surname", surname))] +struct IndexedPerson { + #[unique] + id: i32, + given_name: String, + surname: String, +} + +#[spacetimedb(reducer)] +fn insert_indexed_person(id: i32, given_name: String, surname: String) { + IndexedPerson::insert(IndexedPerson { id, given_name, surname }); +} + +#[spacetimedb(reducer)] +fn delete_indexed_person(id: i32) { + IndexedPerson::delete_by_id(&id); +} + +#[spacetimedb(reducer)] +fn find_indexed_people(surname: String) { + for person in IndexedPerson::filter_by_surname(&surname) { + println!("INDEXED FOUND: id {}: {}, {}", person.id, person.surname, person.given_name); + } +} +""" + + # TODO: split this into multiple test functions + def test_filtering(self): + """Test filtering reducers""" + + self.call("insert_person", 23, "Alice", "al") + self.call("insert_person", 42, "Bob", "bo") + self.call("insert_person", 64, "Bob", "b2") + + # Find a person who is there. + self.call("find_person", 23) + self.assertIn("UNIQUE FOUND: id 23: Alice", self.logs(2)) + + # Find persons with the same name. + self.call("find_person_by_name", "Bob") + logs = self.logs(4) + self.assertIn("UNIQUE FOUND: id 42: Bob aka bo", logs) + self.assertIn("UNIQUE FOUND: id 64: Bob aka b2", logs) + + # Fail to find a person who is not there. + self.call("find_person", 43) + self.assertIn("UNIQUE NOT FOUND: id 43", self.logs(2)) + + # Find a person by nickname. + self.call("find_person_by_nick", "al") + self.assertIn("UNIQUE FOUND: id 23: al", self.logs(2)) + + # Remove a person, and then fail to find them. + self.call("delete_person", 23) + self.call("find_person", 23) + self.assertIn("UNIQUE NOT FOUND: id 23", self.logs(2)) + # Also fail by nickname + self.call("find_person_by_nick", "al") + self.assertIn("UNIQUE NOT FOUND: nick al", self.logs(2)) + + # Add some nonunique people. + self.call("insert_nonunique_person", 23, "Alice", True) + self.call("insert_nonunique_person", 42, "Bob", True) + + # Find a nonunique person who is there. + self.call("find_nonunique_person", 23) + # run_test cargo run logs "$IDENT" 100 + self.assertIn('NONUNIQUE FOUND: id 23: Alice', self.logs(2)) + + # Fail to find a nonunique person who is not there. + self.call("find_nonunique_person", 43) + self.assertNotIn("NONUNIQUE NOT FOUND: id 43", self.logs(2)) + + # Insert a non-human, then find humans, then find non-humans + self.call("insert_nonunique_person", 64, "Jibbitty", False) + self.call("find_nonunique_humans") + self.assertIn('HUMAN FOUND: id 23: Alice', self.logs(2)) + self.assertIn('HUMAN FOUND: id 42: Bob', self.logs(2)) + self.call("find_nonunique_non_humans") + self.assertIn('NON-HUMAN FOUND: id 64: Jibbitty', self.logs(2)) + + # Add another person with the same id, and find them both. + self.call("insert_nonunique_person", 23, "Claire", True) + self.call("find_nonunique_person", 23) + self.assertIn('NONUNIQUE FOUND: id 23: Alice', self.logs(2)) + self.assertIn('NONUNIQUE FOUND: id 23: Claire', self.logs(2)) + + # Check for issues with things present in index but not DB + self.call("insert_person", 101, "Fee", "fee") + self.call("insert_person", 102, "Fi", "fi") + self.call("insert_person", 103, "Fo", "fo") + self.call("insert_person", 104, "Fum", "fum") + self.call("delete_person", 103) + self.call("find_person", 104) + self.assertIn('UNIQUE FOUND: id 104: Fum', self.logs(2)) + + # As above, but for non-unique indices: check for consistency between index and DB + self.call("insert_indexed_person", 7, "James", "Bond") + self.call("insert_indexed_person", 79, "Gold", "Bond") + self.call("insert_indexed_person", 1, "Hydrogen", "Bond") + self.call("insert_indexed_person", 100, "Whiskey", "Bond") + self.call("delete_indexed_person", 100) + self.call("find_indexed_people", "Bond") + logs = self.logs(10) + self.assertIn('INDEXED FOUND: id 7: Bond, James', logs) + self.assertIn('INDEXED FOUND: id 79: Bond, Gold', logs) + self.assertIn('INDEXED FOUND: id 1: Bond, Hydrogen', logs) + self.assertNotIn('INDEXED FOUND: id 100: Bond, Whiskey', logs) + + # Non-unique version; does not work yet, see db_delete codegen in SpacetimeDB\crates\bindings-macro\src\lib.rs + # self.call("insert_nonunique_person", 101, "Fee") + # self.call("insert_nonunique_person", 102, "Fi") + # self.call("insert_nonunique_person", 103, "Fo") + # self.call("insert_nonunique_person", 104, "Fum") + # self.call("find_nonunique_person", 104) + # self.assertIn('NONUNIQUE FOUND: id 104: Fum', self.logs(2)) + + # Filter by Identity + self.call("insert_identified_person", 23, "Alice") + self.call("find_identified_person", 23) + self.assertIn('IDENTIFIED FOUND: Alice', self.logs(2)) + + # Insert row with unique columns twice should fail + self.call("insert_person_twice", 23, "Alice", "al") + self.assertIn('UNIQUE CONSTRAINT VIOLATION ERROR: id 23: Alice', self.logs(2)) diff --git a/smoketests/tests/identity.py b/smoketests/tests/identity.py new file mode 100644 index 0000000000..a1f127c074 --- /dev/null +++ b/smoketests/tests/identity.py @@ -0,0 +1,137 @@ +from .. import Smoketest, random_string, extract_field + +def random_email(): + return random_string() + "@clockworklabs.io" + +class IdentityImports(Smoketest): + AUTOPUBLISH = False + + def setUp(self): + self.reset_config() + + def test_import(self): + """ + Try to import a known good identity to our local ~/.spacetime/config.toml file + + This test does not require a remote spacetimedb instance. + """ + + identity = self.new_identity(email=None) + token = self.token(identity) + + self.reset_config() + + self.fingerprint() + + self.import_identity(identity, token, default=True) + # [ "$(grep "$IDENT" "$TEST_OUT" | awk '{print $1}')" == '***' ] + + def test_new_email(self): + """This test is designed to make sure an email can be set while creating a new identity""" + + # Create a new identity + email = random_email() + identity = self.new_identity(email=email) + token = self.token(identity) + + # Reset our config so we lose this identity + self.reset_config() + + # Import this identity, and set it as the default identity + self.import_identity(identity, token, default=True) + + # Configure our email + output = self.spacetime("identity", "set-email", identity, email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL"), email) + + # Reset config again + self.reset_config() + + # Find our identity by its email + output = self.spacetime("identity", "find", email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) + + def test_remove(self): + """Test deleting an identity from your local ~/.spacetime/config.toml file.""" + + self.fingerprint() + + self.new_identity(email=None) + identity = self.new_identity(email=None) + identities = self.spacetime("identity", "list") + self.assertIn(identity, identities) + + self.spacetime("identity", "remove", identity) + identities = self.spacetime("identity", "list") + self.assertNotIn(identity, identities) + + with self.assertRaises(Exception): + self.spacetime("identity", "remove", identity) + + def test_remove_all(self): + """Test deleting all identities with --force""" + + self.fingerprint() + + identity1 = self.new_identity(email=None) + identity2 = self.new_identity(email=None) + identities = self.spacetime("identity", "list") + self.assertIn(identity2, identities) + + self.spacetime("identity", "remove", identity2) + identities = self.spacetime("identity", "list") + self.assertNotIn(identity2, identities) + + self.spacetime("identity", "remove", "--all", "--force") + identities = self.spacetime("identity", "list") + self.assertNotIn(identity1, identities) + + def test_set_default(self): + """Ensure that we are able to set a default identity""" + + self.fingerprint() + + self.new_identity(email=None) + identity = self.new_identity(email=None) + + identities = self.spacetime("identity", "list").splitlines() + default_identity = next(filter(lambda s: "***" in s, identities), "") + self.assertNotIn(identity, default_identity) + + self.spacetime("identity", "set-default", identity) + + identities = self.spacetime("identity", "list").splitlines() + default_identity = next(filter(lambda s: "***" in s, identities), "") + self.assertIn(identity, default_identity) + + def test_set_email(self): + """Ensure that we are able to associate an email with an identity""" + + self.fingerprint() + + # Create a new identity + identity = self.new_identity(email=None) + email = random_email() + token = self.token(identity) + + # Reset our config so we lose this identity + self.reset_config() + + # Import this identity, and set it as the default identity + self.import_identity(identity, token, default=True) + + # Configure our email + output = self.spacetime("identity", "set-email", identity, email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) + + # Reset config again + self.reset_config() + + # Find our identity by its email + output = self.spacetime("identity", "find", email) + self.assertEqual(extract_field(output, "IDENTITY"), identity) + self.assertEqual(extract_field(output, "EMAIL").lower(), email.lower()) + diff --git a/smoketests/tests/module_nested_op.py b/smoketests/tests/module_nested_op.py new file mode 100644 index 0000000000..ef1f320d95 --- /dev/null +++ b/smoketests/tests/module_nested_op.py @@ -0,0 +1,53 @@ +from .. import Smoketest + +class ModuleNestedOp(Smoketest): + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +pub struct Account { + name: String, + #[unique] + id: i32, +} + +#[spacetimedb(table)] +pub struct Friends { + friend_1: i32, + friend_2: i32, +} + +#[spacetimedb(reducer)] +pub fn create_account(account_id: i32, name: String) { + Account::insert(Account { id: account_id, name } ); +} + +#[spacetimedb(reducer)] +pub fn add_friend(my_id: i32, their_id: i32) { + // Make sure our friend exists + for account in Account::iter() { + if account.id == their_id { + Friends::insert(Friends { friend_1: my_id, friend_2: their_id }); + return; + } + } +} + +#[spacetimedb(reducer)] +pub fn say_friends() { + for friendship in Friends::iter() { + let friend1 = Account::filter_by_id(&friendship.friend_1).unwrap(); + let friend2 = Account::filter_by_id(&friendship.friend_2).unwrap(); + println!("{} is friends with {}", friend1.name, friend2.name); + } +} +""" + + def test_module_nested_op(self): + """This tests uploading a basic module and calling some functions and checking logs afterwards.""" + + self.call("create_account", 1, "House") + self.call("create_account", 2, "Wilson") + self.call("add_friend", 1, 2) + self.call("say_friends") + self.assertIn("House is friends with Wilson", self.logs(2)) diff --git a/smoketests/tests/modules.py b/smoketests/tests/modules.py new file mode 100644 index 0000000000..fc7f4583af --- /dev/null +++ b/smoketests/tests/modules.py @@ -0,0 +1,162 @@ +from .. import Smoketest, random_string +from subprocess import CalledProcessError +import time +import itertools + +class UpdateModule(Smoketest): + AUTOPUBLISH = False + + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +pub struct Person { + #[primarykey] + #[autoinc] + id: u64, + name: String, +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { + Person::insert(Person { id: 0, name }).unwrap(); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + MODULE_CODE_B = """ +use spacetimedb::spacetimedb; + +#[spacetimedb(table)] +pub struct Person { + #[primarykey] + #[autoinc] + id: u64, + name: String, + age: u8, +} +""" + + MODULE_CODE_C = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +pub struct Person { + #[primarykey] + #[autoinc] + id: u64, + name: String, +} + +#[spacetimedb(table)] +pub struct Pet { + species: String, +} + +#[spacetimedb(update)] +pub fn on_module_update() { + println!("MODULE UPDATED"); +} +""" + + + def test_module_update(self): + """Test publishing a module without the --clear-database option""" + + name = random_string() + + self.publish_module(name, clear=False) + + self.call("add", "Robert") + self.call("add", "Julie") + self.call("add", "Samantha") + self.call("say_hello") + logs = self.logs(100) + self.assertIn("Hello, Samantha!", logs) + self.assertIn("Hello, Julie!", logs) + self.assertIn("Hello, Robert!", logs) + self.assertIn("Hello, World!", logs) + + # Unchanged module is ok + self.publish_module(name, clear=False) + + # Changing an existing table isn't + self.write_module_code(self.MODULE_CODE_B) + with self.assertRaises(CalledProcessError) as cm: + self.publish_module(name, clear=False) + self.assertIn("Error: Database update rejected", cm.exception.stderr) + + # Check that the old module is still running by calling say_hello + self.call("say_hello") + + # Adding a table is ok, and invokes update + self.write_module_code(self.MODULE_CODE_C) + self.publish_module(name, clear=False) + self.assertIn("MODULE UPDATED", self.logs(2)) + + +class UploadModule1(Smoketest): + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +pub struct Person { + name: String, +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { + Person::insert(Person { name }); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + + def test_upload_module_1(self): + """This tests uploading a basic module and calling some functions and checking logs afterwards.""" + + self.call("add", "Robert") + self.call("add", "Julie") + self.call("add", "Samantha") + self.call("say_hello") + logs = self.logs(100) + self.assertIn("Hello, Samantha!", logs) + self.assertIn("Hello, Julie!", logs) + self.assertIn("Hello, Robert!", logs) + self.assertIn("Hello, World!", logs) + + +class UploadModule2(Smoketest): + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb, Timestamp}; + +#[spacetimedb(init)] +fn init() { + spacetimedb::schedule!("100ms", my_repeating_reducer(Timestamp::now())); +} + +#[spacetimedb(reducer, repeat = 100ms)] +pub fn my_repeating_reducer(prev: Timestamp) { + println!("Invoked: ts={:?}, delta={:?}", Timestamp::now(), prev.elapsed()); +} +""" + def test_upload_module_2(self): + """This test deploys a module with a repeating reducer and checks the logs to make sure its running.""" + + time.sleep(2) + lines = sum(1 for line in self.logs(100) if "Invoked" in line) + time.sleep(4) + new_lines = sum(1 for line in self.logs(100) if "Invoked" in line) + self.assertLess(lines, new_lines) diff --git a/smoketests/tests/namespaces.py b/smoketests/tests/namespaces.py new file mode 100644 index 0000000000..9391e47b33 --- /dev/null +++ b/smoketests/tests/namespaces.py @@ -0,0 +1,35 @@ +from .. import Smoketest, random_string +import tempfile +import os + + +def count_matches(dir, needle): + count = 0 + for f in os.listdir(dir): + contents = open(os.path.join(dir, f)).read() + count += contents.count(needle) + return count + +class Namespaces(Smoketest): + AUTOPUBLISH = False + + def test_spacetimedb_ns_csharp(self): + """Ensure that the default namespace is working properly""" + + namespace = "SpacetimeDB.Types" + + with tempfile.TemporaryDirectory() as tmpdir: + self.spacetime("generate", "--out-dir", tmpdir, "--lang=cs", "--project-path", self.project_path) + + self.assertEqual(count_matches(tmpdir, f"namespace {namespace}"), 5) + + def test_custom_ns_csharp(self): + """Ensure that when a custom namespace is specified on the command line, it actually gets used in generation""" + + namespace = random_string() + + with tempfile.TemporaryDirectory() as tmpdir: + self.spacetime("generate", "--out-dir", tmpdir, "--lang=cs", "--namespace", namespace, "--project-path", self.project_path) + + self.assertEqual(count_matches(tmpdir, f"namespace {namespace}"), 5) + self.assertEqual(count_matches(tmpdir, "using SpacetimeDB;"), 5) \ No newline at end of file diff --git a/smoketests/tests/new_user_flow.py b/smoketests/tests/new_user_flow.py new file mode 100644 index 0000000000..c9a6caf74e --- /dev/null +++ b/smoketests/tests/new_user_flow.py @@ -0,0 +1,51 @@ +from .. import Smoketest +import time + +class NewUserFlow(Smoketest): + AUTOPUBLISH = False + MODULE_CODE = """ +use spacetimedb::{spacetimedb, println}; + +#[spacetimedb(table)] +pub struct Person { + name: String +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { + Person::insert(Person { name }); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + + def test_new_user_flow(self): + """Test the entirety of the new user flow.""" + + ## Publish your module + self.new_identity(email=None) + + self.publish_module() + + # Calling our database + self.call("say_hello") + self.assertIn("Hello, World!", self.logs(2)) + + ## Calling functions with arguments + self.call("add", "Tyler") + self.call("say_hello") + self.assertEqual(self.logs(5).count("Hello, World!"), 2) + self.assertEqual(self.logs(5).count("Hello, Tyler!"), 1) + + out = self.spacetime("sql", self.address, "SELECT * FROM Person") + self.assertMultiLineEqual(out, """\ + name +------- + Tyler +""") diff --git a/smoketests/tests/panic.py b/smoketests/tests/panic.py new file mode 100644 index 0000000000..41c4014df5 --- /dev/null +++ b/smoketests/tests/panic.py @@ -0,0 +1,32 @@ +from .. import Smoketest + +class Panic(Smoketest): + MODULE_CODE = """ +use spacetimedb::{spacetimedb, println}; +use std::cell::RefCell; + +thread_local! { + static X: RefCell = RefCell::new(0); +} +#[spacetimedb(reducer)] +fn first() { + X.with(|x| { + let _x = x.borrow_mut(); + panic!() + }) +} +#[spacetimedb(reducer)] +fn second() { + X.with(|x| *x.borrow_mut()); + println!("Test Passed"); +} +""" + + def test_panic(self): + """Tests to check if a SpacetimeDB module can handle a panic without corrupting""" + + with self.assertRaises(Exception): + self.call("first") + + self.call("second") + self.assertIn("Test Passed", self.logs(2)) \ No newline at end of file diff --git a/smoketests/tests/permissions.py b/smoketests/tests/permissions.py new file mode 100644 index 0000000000..b8936c2c83 --- /dev/null +++ b/smoketests/tests/permissions.py @@ -0,0 +1,105 @@ +from .. import Smoketest + +class Permissions(Smoketest): + AUTOPUBLISH = False + + def setUp(self): + self.reset_config() + + def test_call(self): + """Ensure that anyone has the permission to call any standard reducer""" + + identity = self.new_identity(email=None) + token = self.token(identity) + + self.publish_module() + + # TODO: can a lot of the usage of reset_config be replaced with just passing -i ? or -a ? + self.reset_config() + self.new_identity(email=None) + self.call("say_hello") + + self.reset_config() + self.import_identity(identity, token, default=True) + self.assertEqual("\n".join(self.logs(10000)).count("World"), 1) + + def test_delete(self): + """Ensure that you cannot delete a database that you do not own""" + + identity = self.new_identity(email=None, default=True) + + self.publish_module() + + self.reset_config() + with self.assertRaises(Exception): + self.spacetime("delete", self.address) + + def test_describe(self): + """Ensure that anyone can describe any database""" + + self.new_identity(email=None) + self.publish_module() + + self.reset_config() + self.new_identity(email=None) + self.spacetime("describe", self.address) + + def test_describe(self): + """Ensure that we are not able to view the logs of a module that we don't have permission to view""" + + self.new_identity(email=None) + self.publish_module() + + self.reset_config() + self.new_identity(email=None) + self.call("say_hello") + + self.reset_config() + identity = self.new_identity(email=None, default=True) + with self.assertRaises(Exception): + self.spacetime("logs", self.address, "10000") + + def test_publish(self): + """This test checks to make sure that you cannot publish to an address that you do not own.""" + + self.new_identity(email=None, default=True) + self.publish_module() + + self.reset_config() + + with self.assertRaises(Exception): + self.spacetime("publish", self.address, "--project-path", self.project_path, "--clear-database") + + +class PrivateTablePermissions(Smoketest): + MODULE_CODE = """ +use spacetimedb::spacetimedb; + +#[spacetimedb(table)] +#[sats(name = "_Secret")] +pub struct Secret { + answer: u8, +} + +#[spacetimedb(init)] +pub fn init() { + Secret::insert(Secret { answer: 42 }); +} +""" + + def test_private_table(self): + """Ensure that a private table can only be queried by the database owner""" + + out = self.spacetime("sql", self.address, "select * from _Secret") + self.assertMultiLineEqual(out, """\ + answer +-------- + 42 +""") + + self.reset_config() + self.new_identity(email=None) + + with self.assertRaises(Exception): + self.spacetime("sql", self.address, "select * from _Secret") + diff --git a/smoketests/tests/servers.py b/smoketests/tests/servers.py new file mode 100644 index 0000000000..9c541926ee --- /dev/null +++ b/smoketests/tests/servers.py @@ -0,0 +1,25 @@ +from .. import Smoketest, extract_field +import re + +class Servers(Smoketest): + AUTOPUBLISH = False + + def test_servers(self): + """Verify that we can add and list server configurations""" + + out = self.spacetime("server", "add", "https://testnet.spacetimedb.com", "testnet", "--no-fingerprint") + self.assertEqual(extract_field(out, "Host:"), "testnet.spacetimedb.com") + self.assertEqual(extract_field(out, "Protocol:"), "https") + + servers = self.spacetime("server", "list") + self.assertRegex(servers, re.compile(r"^\s*testnet\.spacetimedb\.com\s+https\s+testnet\s*$", re.M)) + self.assertRegex(servers, re.compile(r"^\s*\*\*\*\s+127\.0\.0\.1:3000\s+http\s+localhost\s*$", re.M)) + + out = self.spacetime("server", "fingerprint", "127.0.0.1:3000", "-f") + self.assertIn("No saved fingerprint for server 127.0.0.1:3000.", out) + + out = self.spacetime("server", "fingerprint", "127.0.0.1:3000") + self.assertIn("Fingerprint is unchanged for server 127.0.0.1:3000", out) + + out = self.spacetime("server", "fingerprint", "localhost") + self.assertIn("Fingerprint is unchanged for server localhost", out) \ No newline at end of file diff --git a/test/tests/sql-format.sh b/smoketests/tests/sql.py similarity index 58% rename from test/tests/sql-format.sh rename to smoketests/tests/sql.py index cdb565cd93..9165b31d03 100644 --- a/test/tests/sql-format.sh +++ b/smoketests/tests/sql.py @@ -1,15 +1,7 @@ -#!/bin/bash +from .. import Smoketest -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test is designed to test the format of the output of sql queries" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" <1{print $NF}')" + self.call("test") + sql_out = self.spacetime("sql", self.address, "SELECT * FROM BuiltIn") -# We have to give the database some time to setup our instance -sleep 2 - -# Calling our database -run_test cargo run call "$ADDRESS" test -run_test cargo run sql "$ADDRESS" "SELECT * FROM BuiltIn" - -[ "$(cat "$TEST_OUT" | tail -n 3)" == \ -' a_b | a_i8 | a_i16 | a_i32 | a_i64 | a_i128 | a_u8 | a_u16 | a_u32 | a_u64 | a_u128 | a_f32 | a_f64 | a_str | a_bytes | a_tuple '$'\n'\ -'------+------+-------+--------+----------+---------------+------+-------+-------+----------+---------------+-----------+--------------------+---------------------+------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------'$'\n'\ -' true | -25 | -3224 | -23443 | -2344353 | -234434897853 | 105 | 1050 | 83892 | 48937498 | 4378528978889 | 594806.56 | -3454353.345389043 | This is spacetimedb | 0x01020304050607 | (a_b = true, a_i8 = -25, a_i16 = -3224, a_i32 = -23443, a_i64 = -2344353, a_i128 = -234434897853, a_u8 = 105, a_u16 = 1050, a_u32 = 83892, a_u64 = 48937498, a_u128 = 4378528978889, a_f32 = 594806.56, a_f64 = -3454353.345389043, a_str = "This is spacetimedb", a_bytes = 0x01020304050607) ' ] + self.assertMultiLineEqual(sql_out, """\ + a_b | a_i8 | a_i16 | a_i32 | a_i64 | a_i128 | a_u8 | a_u16 | a_u32 | a_u64 | a_u128 | a_f32 | a_f64 | a_str | a_bytes | a_tuple +------+------+-------+--------+----------+---------------+------+-------+-------+----------+---------------+-----------+--------------------+---------------------+------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + true | -25 | -3224 | -23443 | -2344353 | -234434897853 | 105 | 1050 | 83892 | 48937498 | 4378528978889 | 594806.56 | -3454353.345389043 | This is spacetimedb | 0x01020304050607 | (a_b = true, a_i8 = -25, a_i16 = -3224, a_i32 = -23443, a_i64 = -2344353, a_i128 = -234434897853, a_u8 = 105, a_u16 = 1050, a_u32 = 83892, a_u64 = 48937498, a_u128 = 4378528978889, a_f32 = 594806.56, a_f64 = -3454353.345389043, a_str = "This is spacetimedb", a_bytes = 0x01020304050607) +""") diff --git a/test/tests/template b/smoketests/tests/template similarity index 100% rename from test/tests/template rename to smoketests/tests/template diff --git a/smoketests/tests/zz_docker.py b/smoketests/tests/zz_docker.py new file mode 100644 index 0000000000..309e8e57b2 --- /dev/null +++ b/smoketests/tests/zz_docker.py @@ -0,0 +1,135 @@ +from .. import Smoketest, run_cmd, requires_docker +from urllib.request import urlopen, URLError + + +def restart_docker(): + # Behold! + # + # You thought stop/start restarts? How wrong. Restart restarts. + run_cmd("docker", "compose", "restart") + # The suspense! + # + # Wait until compose believes the health probe succeeds. + # + # The container may decide to recompile, or grab a coffee at crates.io, or + # whatever. In any case, restart doesn't mean the server is up yet. + run_cmd("docker", "compose", "up", "--no-recreate", "--detach", "--wait-timeout", "60") + # Belts and suspenders! + # + # The health probe runs inside the container, but that doesn't mean we can + # reach it from outside. Ping until we get through. + ping() + +def ping(): + tries = 0 + host = "127.0.0.1:3000" + while tries < 5: + tries += 1 + try: + urlopen(f"http://{host}/database/ping") + break + except URLError: + print("Server down") + else: + raise Exception(f"Server at {host} not responding") + print(f"Server up after {tries} try") + + +@requires_docker +class DockerRestartModule(Smoketest): + # Note: creating indexes on `Person` + # exercises more possible failure cases when replaying after restart + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +#[spacetimedb(index(btree, name = "name_idx", name))] +pub struct Person { + #[primarykey] + #[autoinc] + id: u32, + name: String, +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { +Person::insert(Person { id: 0, name }).unwrap(); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + + def test_restart_module(self): + """This tests to see if SpacetimeDB can be queried after a restart""" + + self.call("add", "Robert") + + restart_docker() + + self.call("add", "Julie") + self.call("add", "Samantha") + self.call("say_hello") + logs = self.logs(100) + self.assertIn("Hello, Samantha!", logs) + self.assertIn("Hello, Julie!", logs) + self.assertIn("Hello, Robert!", logs) + self.assertIn("Hello, World!", logs) + + +@requires_docker +class DockerRestartSql(Smoketest): + # Note: creating indexes on `Person` + # exercises more possible failure cases when replaying after restart + MODULE_CODE = """ +use spacetimedb::{println, spacetimedb}; + +#[spacetimedb(table)] +#[spacetimedb(index(btree, name = "name_idx", name))] +pub struct Person { + #[primarykey] + #[autoinc] + id: u32, + name: String, +} + +#[spacetimedb(reducer)] +pub fn add(name: String) { +Person::insert(Person { id: 0, name }).unwrap(); +} + +#[spacetimedb(reducer)] +pub fn say_hello() { + for person in Person::iter() { + println!("Hello, {}!", person.name); + } + println!("Hello, World!"); +} +""" + + def test_restart_module(self): + """This tests to see if SpacetimeDB can be queried after a restart""" + + self.call("add", "Robert") + self.call("add", "Julie") + self.call("add", "Samantha") + self.call("say_hello") + logs = self.logs(100) + self.assertIn("Hello, Samantha!", logs) + self.assertIn("Hello, Julie!", logs) + self.assertIn("Hello, Robert!", logs) + self.assertIn("Hello, World!", logs) + + restart_docker() + + sql_out = self.spacetime("sql", self.address, "SELECT name FROM Person WHERE id = 3") + self.assertMultiLineEqual(sql_out, """\ + name +---------- + Samantha +""") \ No newline at end of file diff --git a/smoketests/unittest_parallel.py b/smoketests/unittest_parallel.py new file mode 100644 index 0000000000..24395a0c71 --- /dev/null +++ b/smoketests/unittest_parallel.py @@ -0,0 +1,376 @@ +# vendored and modified from unittest-parallel by Craig Hobbs +# TODO: upstream some of these changes? to make it usable as a library, maybe? + +# Licensed under the MIT License +# https://github.com/craigahobbs/unittest-parallel/blob/main/LICENSE +# full text of license file below: +# +# The MIT License (MIT) +# +# Copyright (c) 2017 Craig A. Hobbs +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +unittest-parallel command-line script main module +""" + +import argparse +from contextlib import contextmanager +from io import StringIO +import os +import sys +import tempfile +import time +import unittest +import concurrent.futures +import threading + +import coverage + + +def main(**kwargs): + """ + unittest-parallel command-line script main entry point + """ + + # Command line arguments + parser = argparse.ArgumentParser(prog='unittest-parallel') + parser.add_argument('-v', '--verbose', action='store_const', const=2, default=1, + help='Verbose output') + parser.add_argument('-q', '--quiet', dest='verbose', action='store_const', const=0, default=1, + help='Quiet output') + parser.add_argument('-f', '--failfast', action='store_true', default=False, + help='Stop on first fail or error') + parser.add_argument('-b', '--buffer', action='store_true', default=False, + help='Buffer stdout and stderr during tests') + parser.add_argument('-k', dest='testNamePatterns', action='append', type=_convert_select_pattern, + help='Only run tests which match the given substring') + parser.add_argument('-s', '--start-directory', metavar='START', default='.', + help="Directory to start discovery ('.' default)") + parser.add_argument('-p', '--pattern', metavar='PATTERN', default='test*.py', + help="Pattern to match tests ('test*.py' default)") + parser.add_argument('-t', '--top-level-directory', metavar='TOP', + help='Top level directory of project (defaults to start directory)') + group_parallel = parser.add_argument_group('parallelization options') + group_parallel.add_argument('-j', '--jobs', metavar='COUNT', type=int, default=0, + help='The number of test processes (default is 0, all cores)') + group_parallel.add_argument('--level', choices=['module', 'class', 'test'], default='module', + help="Set the test parallelism level (default is 'module')") + group_parallel.add_argument('--disable-process-pooling', action='store_true', default=False, + help='Do not reuse processes used to run test suites') + group_coverage = parser.add_argument_group('coverage options') + group_coverage.add_argument('--coverage', action='store_true', + help='Run tests with coverage') + group_coverage.add_argument('--coverage-branch', action='store_true', + help='Run tests with branch coverage') + group_coverage.add_argument('--coverage-rcfile', metavar='RCFILE', + help='Specify coverage configuration file') + group_coverage.add_argument('--coverage-include', metavar='PAT', action='append', + help='Include only files matching one of these patterns. Accepts shell-style (quoted) wildcards.') + group_coverage.add_argument('--coverage-omit', metavar='PAT', action='append', + help='Omit files matching one of these patterns. Accepts shell-style (quoted) wildcards.') + group_coverage.add_argument('--coverage-source', metavar='SRC', action='append', + help='A list of packages or directories of code to be measured') + group_coverage.add_argument('--coverage-html', metavar='DIR', + help='Generate coverage HTML report') + group_coverage.add_argument('--coverage-xml', metavar='FILE', + help='Generate coverage XML report') + group_coverage.add_argument('--coverage-fail-under', metavar='MIN', type=float, + help='Fail if coverage percentage under min') + args = parser.parse_args(args=[]) + args.__dict__.update(kwargs) + if args.coverage_branch: + args.coverage = args.coverage_branch + + process_count = max(0, args.jobs) + if process_count == 0: + process_count = os.cpu_count() + + # Create the temporary directory (for coverage files) + with tempfile.TemporaryDirectory() as temp_dir: + + # Discover tests + # with _coverage(args, temp_dir): + # test_loader = unittest.TestLoader() + # if args.testNamePatterns: + # test_loader.testNamePatterns = args.testNamePatterns + # discover_suite = test_loader.discover(args.start_directory, pattern=args.pattern, top_level_dir=args.top_level_directory) + discover_suite = args.discovered_tests + + # Get the parallelizable test suites + if args.level == 'test': + test_suites = list(_iter_test_cases(discover_suite)) + elif args.level == 'class': + test_suites = list(_iter_class_suites(discover_suite)) + else: # args.level == 'module' + test_suites = list(_iter_module_suites(discover_suite)) + + # Don't use more processes than test suites + process_count = max(1, min(len(test_suites), process_count)) + + # Report test suites and processes + print( + f'Running {len(test_suites)} test suites ({discover_suite.countTestCases()} total tests) across {process_count} threads', + file=sys.stderr + ) + if args.verbose > 1: + print(file=sys.stderr) + + # Run the tests in parallel + start_time = time.perf_counter() + with concurrent.futures.ThreadPoolExecutor(max_workers=process_count) as executor: + test_manager = ParallelTestManager(args, temp_dir) + + futures = [executor.submit(test_manager.run_tests, suite) for suite in discover_suite] + + # Aggregate parallel test run results + tests_run = 0 + errors = [] + failures = [] + skipped = 0 + expected_failures = 0 + unexpected_successes = 0 + + for fut in concurrent.futures.as_completed(futures): + try: + result, stream = fut.result() + except concurrent.futures.CancelledError: + continue + tests_run += result.testsRun + errors.extend(ParallelTestManager._format_error(result, error) for error in result.errors) + failures.extend(ParallelTestManager._format_error(result, failure) for failure in result.failures) + skipped += len(result.skipped) + expected_failures += len(result.expectedFailures) + unexpected_successes += len(result.unexpectedSuccesses) + if result.shouldStop: + for fut in futures: + fut.cancel() + + is_success = not(errors or failures or unexpected_successes) + + stop_time = time.perf_counter() + test_duration = stop_time - start_time + + # Compute test info + infos = [] + if failures: + infos.append(f'failures={len(failures)}') + if errors: + infos.append(f'errors={len(errors)}') + if skipped: + infos.append(f'skipped={skipped}') + if expected_failures: + infos.append(f'expected failures={expected_failures}') + if unexpected_successes: + infos.append(f'unexpected successes={unexpected_successes}') + + # Report test errors + if errors or failures: + print(file=sys.stderr) + for error in errors: + print(error, file=sys.stderr) + for failure in failures: + print(failure, file=sys.stderr) + elif args.verbose > 0: + print(file=sys.stderr) + + # Test report + print(unittest.TextTestResult.separator2, file=sys.stderr) + print(f'Ran {tests_run} {"tests" if tests_run > 1 else "test"} in {test_duration:.3f}s', file=sys.stderr) + print(file=sys.stderr) + print(f'{"OK" if is_success else "FAILED"}{" (" + ", ".join(infos) + ")" if infos else ""}', file=sys.stderr) + + # Return an error status on failure + if not is_success: + parser.exit(status=len(errors) + len(failures) + unexpected_successes) + + # Coverage? + if args.coverage: + + # Combine the coverage files + cov_options = {} + if args.coverage_rcfile is not None: + cov_options['config_file'] = args.coverage_rcfile + cov = coverage.Coverage(**cov_options) + cov.combine(data_paths=[os.path.join(temp_dir, x) for x in os.listdir(temp_dir)]) + + # Coverage report + print(file=sys.stderr) + percent_covered = cov.report(ignore_errors=True, file=sys.stderr) + print(f'Total coverage is {percent_covered:.2f}%', file=sys.stderr) + + # HTML coverage report + if args.coverage_html: + cov.html_report(directory=args.coverage_html, ignore_errors=True) + + # XML coverage report + if args.coverage_xml: + cov.xml_report(outfile=args.coverage_xml, ignore_errors=True) + + # Fail under + if args.coverage_fail_under and percent_covered < args.coverage_fail_under: + parser.exit(status=2) + + +def _convert_select_pattern(pattern): + if not '*' in pattern: + return f'*{pattern}*' + return pattern + + +@contextmanager +def _coverage(args, temp_dir): + # Running tests with coverage? + if args.coverage: + # Generate a random coverage data file name - file is deleted along with containing directory + with tempfile.NamedTemporaryFile(dir=temp_dir, delete=False) as coverage_file: + pass + + # Create the coverage object + cov_options = { + 'branch': args.coverage_branch, + 'data_file': coverage_file.name, + 'include': args.coverage_include, + 'omit': (args.coverage_omit if args.coverage_omit else []) + [__file__], + 'source': args.coverage_source + } + if args.coverage_rcfile is not None: + cov_options['config_file'] = args.coverage_rcfile + cov = coverage.Coverage(**cov_options) + try: + # Start measuring code coverage + cov.start() + + # Yield for unit test running + yield cov + finally: + # Stop measuring code coverage + cov.stop() + + # Save the collected coverage data to the data file + cov.save() + else: + # Not running tests with coverage - yield for unit test running + yield None + + +# Iterate module-level test suites - all top-level test suites returned from TestLoader.discover +def _iter_module_suites(test_suite): + for module_suite in test_suite: + if module_suite.countTestCases(): + yield module_suite + + +# Iterate class-level test suites - test suites that contains test cases +def _iter_class_suites(test_suite): + has_cases = any(isinstance(suite, unittest.TestCase) for suite in test_suite) + if has_cases: + yield test_suite + else: + for suite in test_suite: + yield from _iter_class_suites(suite) + + +# Iterate test cases (methods) +def _iter_test_cases(test_suite): + if isinstance(test_suite, unittest.TestCase): + yield test_suite + else: + for suite in test_suite: + yield from _iter_test_cases(suite) + + +class ParallelTestManager: + + def __init__(self, args, temp_dir): + self.args = args + self.temp_dir = temp_dir + + def run_tests(self, test_suite): + # Run unit tests + with _coverage(self.args, self.temp_dir): + stream = StringIO() + runner = unittest.TextTestRunner( + stream=stream, + resultclass=ParallelTextTestResult, + verbosity=self.args.verbose, + failfast=self.args.failfast, + buffer=self.args.buffer + ) + result = runner.run(test_suite) + + # Return (test_count, errors, failures, skipped_count, expected_failure_count, unexpected_success_count) + return result, stream + + @staticmethod + def _format_error(result, error): + return '\n'.join([ + unittest.TextTestResult.separator1, + result.getDescription(error[0]), + unittest.TextTestResult.separator2, + error[1] + ]) + + +class ParallelTextTestResult(unittest.TextTestResult): + + def __init__(self, stream, descriptions, verbosity): + stream = type(stream)(sys.stderr) + super().__init__(stream, descriptions, verbosity) + + def startTest(self, test): + if self.showAll: + self.stream.writeln(f'{self.getDescription(test)} ...') + self.stream.flush() + super(unittest.TextTestResult, self).startTest(test) + + def _add_helper(self, test, dots_message, show_all_message): + if self.showAll: + self.stream.writeln(f'{self.getDescription(test)} ... {show_all_message}') + elif self.dots: + self.stream.write(dots_message) + self.stream.flush() + + def addSuccess(self, test): + super(unittest.TextTestResult, self).addSuccess(test) + self._add_helper(test, '.', 'ok') + + def addError(self, test, err): + super(unittest.TextTestResult, self).addError(test, err) + self._add_helper(test, 'E', 'ERROR') + + def addFailure(self, test, err): + super(unittest.TextTestResult, self).addFailure(test, err) + self._add_helper(test, 'F', 'FAIL') + + def addSkip(self, test, reason): + super(unittest.TextTestResult, self).addSkip(test, reason) + self._add_helper(test, 's', f'skipped {reason!r}') + + def addExpectedFailure(self, test, err): + super(unittest.TextTestResult, self).addExpectedFailure(test, err) + self._add_helper(test, 'x', 'expected failure') + + def addUnexpectedSuccess(self, test): + super(unittest.TextTestResult, self).addUnexpectedSuccess(test) + self._add_helper(test, 'u', 'unexpected success') + + def printErrors(self): + pass diff --git a/test/config.toml b/test/config.toml deleted file mode 100644 index e74afa7cbe..0000000000 --- a/test/config.toml +++ /dev/null @@ -1,7 +0,0 @@ -default_server = '127.0.0.1:3000' -identity_configs = [] - -[[server_configs]] -host = '127.0.0.1:3000' -protocol = 'http' -nickname = 'localhost' diff --git a/test/lib.include b/test/lib.include deleted file mode 100644 index 9624a33e67..0000000000 --- a/test/lib.include +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash - -# Runs a test with the assumption that it will return a zero result code -run_test() { - set +e - "$@" > "$TEST_OUT" 2>&1 - RESULT=$? - cat "$TEST_OUT" - set -e - return "$RESULT" -} - -# Runs a test with the assumption that it will return a non-zero result code -run_fail_test() { - if "$@" > "$TEST_OUT" 2>&1 ; then - cat "$TEST_OUT" - return 1 - fi - cat "$TEST_OUT" - return 0 -} - -# This resets the spacetime config for a new test run -reset_config() { - SPACETIME_CONFIG_FILE="$(mktemp)" - export SPACETIME_CONFIG_FILE - cp "$RESET_SPACETIME_CONFIG" "$SPACETIME_CONFIG_FILE" -} - -# This deletes the project from the previous test run -reset_project() { - PROJECT_PATH="$(mktemp -d)" - rmdir "$PROJECT_PATH" - cp -rp "$RESET_PROJECT_PATH" "$PROJECT_PATH" - cp "$SPACETIME_DIR"/rust-toolchain.toml "$PROJECT_PATH" - export PROJECT_PATH -} - -# This cleans up the temporary project directory after a test -clear_project() { - rm -rf "$PROJECT_PATH" -} - -# This cleans up the temporary config file after a test -clear_config() { - rm -f "$SPACETIME_CONFIG_FILE" -} - -random_string() { - if [[ "$OSTYPE" == "darwin"* ]]; then - echo $RANDOM | md5 -q | head -c 20 - else - echo $RANDOM | md5sum | head -c 20 - fi -} - -spacetime_publish() { - RETURN_DIR=$PWD - cd "$SPACETIME_DIR" - set +e - run_test spacetime publish "$@" - RESULT_CODE=$? - cd "$RETURN_DIR" || exit 1 - set -e - return "$RESULT_CODE" -} - -fsed() { - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i.sed_bak "$@" - rm -f rm *.sed_bak - else - sed -i "$@" - fi -} - -restart_docker() { - # Behold! - # - # You thought stop/start restarts? How wrong. Restart restarts. - docker compose restart - # The suspense! - # - # Wait until compose believes the health probe succeeds. - # - # The container may decide to recompile, or grab a coffee at crates.io, or - # whatever. In any case, restart doesn't mean the server is up yet. - docker compose up --no-recreate --detach --wait-timeout 60 - # Belts and suspenders! - # - # The health probe runs inside the container, but that doesn't mean we can - # reach it from outside. Ping until we get through. - ping -} - -ping() { - local retries=5 - local success=0 - while true - do - curl -sf http://127.0.0.1:3000/database/ping && { success=1; break; } || echo "Server down" - retries=$((retries - 1)) - if [ $retries -gt 0 ] - then - sleep 5 - else - break - fi - done - if [ $success -lt 1 ] - then - echo "Server at 127.0.0.1:3000 not responding" - exit 127 - else - echo "Server up after $((5 - retries)) retries" - fi -} - -# vim: noexpandtab tabstop=4 shiftwidth=4 diff --git a/test/run-smoke-tests.sh b/test/run-smoke-tests.sh deleted file mode 100755 index 27b0bacd93..0000000000 --- a/test/run-smoke-tests.sh +++ /dev/null @@ -1,329 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# set -x - -cd "$(dirname "$0")" - -SPACETIME_CARGO_PROFILE=release-fast -CRST='\033[0m' # Text Reset -GRN='\033[0;32m' # Green -RED='\033[0;31m' # Red -passed_tests=() -failed_tests=() -RESET_SPACETIME_CONFIG=$(mktemp) -export RESET_SPACETIME_CONFIG -export SPACETIME_DIR="$PWD/.." -RUN_PARALLEL=false - -declare -a TESTS -for test in tests/*.sh ; do - file_name="$(basename "$test")" - TESTS+=("${file_name%.*}") -done - -EXCLUDE_TESTS=() - -while [ $# != 0 ] ; do - case $1 in - -x) - shift - EXCLUDE_TESTS+=("$@") - break - ;; - --parallel) - shift - RUN_PARALLEL=true - echo "Running tests in parallel." - ;; - *) - TESTS=("$@") - break - ;; - esac -done - -# Make sure the active toolchain is installed including the wasm target (as -# specified in rust-toolchain.toml), and clippy. Do NOT try to update other -# toolchains people may have on their system, nor rustup itself -- this can take -# a long time, e.g. when people have "nightly" installed without a specific -# version. -# -# Note: `show active-toolchain` installs the toolchain if it is missing, -# `update` adds the wasm target if the toolchain is there but without that -# target. -rustup update --no-self-update "$(rustup show active-toolchain|cut -d' ' -f1)" -rustup component add clippy - -source "lib.include" -cp ./config.toml "$RESET_SPACETIME_CONFIG" - -cd .. -export SPACETIME_HOME=$PWD - -# Create a project that we can copy to reset our project -RESET_PROJECT_PATH=$(mktemp -d) -export RESET_PROJECT_PATH -cargo run init "$RESET_PROJECT_PATH" --lang rust -# We have to force using the local spacetimedb_bindings otherwise we will download them from crates.io -if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s@.*spacetimedb.*=.*@spacetimedb = { path = \"${SPACETIME_DIR}/crates/bindings\" }@g" "${RESET_PROJECT_PATH}/Cargo.toml" -elif [[ "$OSTYPE" == "msys"* ]]; then - # Running in git bash; do horrible path conversion; yes we do need all of those - WINPATH="$(cygpath -w "${SPACETIME_DIR}/crates/bindings" | sed 's/\\/\\\\\\\\/g')" - sed -i "s@.*spacetimedb.*=.*@spacetimedb = { path = \"${WINPATH}\" }@g" "${RESET_PROJECT_PATH}/Cargo.toml" -else - sed -i "s@.*spacetimedb.*=.*@spacetimedb = { path = \"${SPACETIME_DIR}/crates/bindings\" }@g" "${RESET_PROJECT_PATH}/Cargo.toml" -fi - -cargo run build "$RESET_PROJECT_PATH" -s -d - -export SPACETIME_SKIP_CLIPPY=1 - -if [ -z "${NO_DOCKER:-}" ] ; then - if [ "$(docker ps | grep "node" -c)" != 1 ] ; then - echo "Docker container not found, is SpacetimeDB running?" - exit 1 - fi - - CONTAINER_NAME=$(docker ps | grep "node" | awk '{print $NF}') - docker logs "$CONTAINER_NAME" -fi - -execute_procedural_test() { - if [ $# != 1 ] ; then - echo "Usage: execute_procedural_test " - exit 1 - fi - - test_name=$1 - - reset_config - reset_project - - test_out_file=$(mktemp) - printf " **************** Running %s... " "$test_name" - execute_test "$test_name" "$test_out_file" - result_code=$? - set -e - if [ $result_code == 0 ] ; then - printf "${GRN}PASS${CRST}\n" - else - printf "${RED}FAIL${CRST}\n" - fi - process_test_result "$test_name" "$result_code" "$PROJECT_PATH" "$test_out_file" "$SPACETIME_CONFIG_FILE" -} - -# Note: $PROJECT_PATH and $SPACETIME_CONFIG_FILE are implicit environment variable inputs to this function. -# Before calling this function you should have already run `reset_project` and `reset_config` yourself. -execute_test() { - if [ "$#" != 2 ] ; then - echo "Usage: execute_test " - exit 1 - fi - [ -d "$PROJECT_PATH" ] - [ -f "$SPACETIME_CONFIG_FILE" ] - - test_name=$1 - test_path="test/tests/$test_name.sh" - test_out_file=$2 - - # TODO: Remove this, we shouldn't be using this TEST_OUT variable anymore because it - # basically has no function. We should just be using piping where needed - TEST_OUT=$(mktemp) - export TEST_OUT - # end todo - - set +e - bash -x "$test_path" > "$test_out_file" 2>&1 - result_code=$? - set -e - echo "Test Out:" >> "$test_out_file" - cat "$TEST_OUT" >> "$test_out_file" - - rm -f "$TEST_OUT" - # if ! bash -x "$test_path" ; then - set +e - return $result_code -} - -# Prints the result of a test. If the test failed, then the test output is printed. -process_test_result() { - if [ $# != 5 ] ; then - echo "Usage: process_test_result " - exit 1 - fi - - test_name=$1 - result_code=$2 - PROJECT_PATH=$3 - out_file_path=$4 - config_file_path=$5 - - if [ "$result_code" == 0 ] ; then - passed_tests+=("$test_name") - - # Cleanup the test execution only if the test passed - rm -rf "$PROJECT_PATH" "$out_file_path" "$config_file_path" - else - [ -z "${NO_DOCKER:-}" ] && docker logs "$CONTAINER_NAME" - cat "$out_file_path" - echo "Config file:" - cat "$config_file_path" - echo "PROJECT_PATH=$PROJECT_PATH TEST_OUT=$out_file_path SPACETIME_CONFIG_FILE=$config_file_path" - failed_tests+=("$test_name") - fi -} - -list_contains() { - local a=$1 - shift - for x in "$@"; do - if [[ "$x" == "$a" ]]; then - return 0 - fi - done - return 1 -} - - -# Arrays used for running tests procedurally -TESTS_PID=() -TESTS_OUT_FILE=() -TESTS_NAME=() -TESTS_PROJECT_PATH=() -TESTS_CONFIG_FILE=() - -# Make sure background tests are torn down even if we don't get to `wait` -# for them (e.g. on ^C). Mainly for $RUN_PARALLEL. -terminate_jobs() { - local running="" - running="$(jobs -pr)" - if [ -n "$running" ]; then - kill "$running" - fi -} -trap 'terminate_jobs' SIGINT SIGTERM EXIT - -for smoke_test in "${TESTS[@]}" ; do - if [ ${#EXCLUDE_TESTS[@]} -ne 0 ] && list_contains "$smoke_test" "${EXCLUDE_TESTS[@]}"; then - echo "Skipping test $smoke_test" - continue - fi - if [ -f "./test/tests/$smoke_test.sh" ]; then - if [ "$RUN_PARALLEL" == "true" ] ; then - # Skip any non-parallizable tests if we're running in parallel - if [[ "$smoke_test" == zz_* ]] ; then - continue - fi - - TESTS_NAME+=("$smoke_test") - reset_config - TESTS_CONFIG_FILE+=("$SPACETIME_CONFIG_FILE") - - reset_project - TESTS_PROJECT_PATH+=("$PROJECT_PATH") - - test_out_file=$(mktemp) - TESTS_OUT_FILE+=("$test_out_file") - - (execute_test "$smoke_test" "$test_out_file") & - TESTS_PID+=($!) - echo "[PARALLEL] Test started: $smoke_test" - else - execute_procedural_test "$smoke_test" - fi - else - echo "Unknown test: $smoke_test" - exit 1 - fi -done - - -if [ "$RUN_PARALLEL" == "true" ] ; then - # Wait for all processes to end, and save their exit codes - length=${#TESTS_PID[@]} - while true ; do - FOUND=0 - for ((i=0; i/dev/null; then - continue - fi - - out_file=${TESTS_OUT_FILE[$i]} - test_name=${TESTS_NAME[$i]} - project_path=${TESTS_PROJECT_PATH[$i]} - config_file=${TESTS_CONFIG_FILE[$i]} - set +e - wait "$pid" - result_code=$? - set -e - if [ $result_code == 0 ] ; then - printf "[${GRN}PASS${CRST}] | $test_name finished\n" - else - printf "[${RED}FAIL${CRST}] | $test_name finished\n" - fi - process_test_result "$test_name" "$result_code" "$project_path" "$out_file" "$config_file" - TESTS_PID[i]="" - done - - if [ $FOUND == 0 ] ; then - break; - fi - done - - # Now run any tests that cannot be parallelized - for smoke_test in "${TESTS[@]}" ; do - if [ ${#EXCLUDE_TESTS[@]} -ne 0 ] && list_contains "$smoke_test" "${EXCLUDE_TESTS[@]}"; then - continue - fi - - if [[ "$smoke_test" == zz_* ]]; then - if [ -f "./test/tests/$smoke_test.sh" ]; then - execute_procedural_test "$smoke_test" - else - echo "Unknown test: $smoke_test" - exit 1 - fi - fi - - done -fi - -printf "\n\n*************************\n" -printf "** Smoke Tests Summary **\n" -printf "*************************\n\n" - -if [ ${#passed_tests[@]} -ne 0 ]; then - printf "${GRN}Passed${CRST} Tests:\n" - for t in "${passed_tests[@]}" ; do - echo " $t" - done -fi - -if [ ${#failed_tests[@]} -eq 0 ] ; then - printf "\nNo failures reported.\n\n" -else - printf "\n${RED}Failed${CRST} Tests:\n" - for t in "${failed_tests[@]}" ; do - echo " $t" - done - - printf "\nDescriptions for failed tests:\n" - for t in "${failed_tests[@]}" ; do - printf "\n%s\n\t" "$t" - DESCRIBE_TEST=1 bash "test/tests/${t}.sh" - done - - exit 1 -fi - -# vim: noexpandtab tabstop=4 shiftwidth=4 diff --git a/test/tests/autoinc1.sh b/test/tests/autoinc1.sh deleted file mode 100644 index 71b5e4c83b..0000000000 --- a/test/tests/autoinc1.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests the autoinc functionality" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -do_test() { - echo "RUNNING TEST FOR VALUE: $1" - reset_project - - cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -pub struct Person { - #[autoinc] - key_col: REPLACE_VALUE, - name: String, -} - -#[spacetimedb(reducer)] -pub fn add(name: String, expected_value: REPLACE_VALUE) { - let value = Person::insert(Person { key_col: 0, name }); - assert_eq!(value.key_col, expected_value); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}:{}!", person.key_col, person.name); - } - println!("Hello, World!"); -} -EOF - - fsed "s/REPLACE_VALUE/$1/g" "${PROJECT_PATH}/src/lib.rs" - - run_test cargo run publish --project-path "$PROJECT_PATH" --clear-database --skip_clippy - [ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] - IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - - run_test cargo run call "$IDENT" add Robert 1 - run_test cargo run call "$IDENT" add Julie 2 - run_test cargo run call "$IDENT" add Samantha 3 - run_test cargo run call "$IDENT" say_hello - run_test cargo run logs "$IDENT" 100 - [[ "$(grep 'Samantha' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 3:Samantha! ]] - [[ "$(grep 'Julie' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 2:Julie! ]] - [[ "$(grep 'Robert' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 1:Robert! ]] - [[ "$(grep 'World' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ World! ]] - - clear_project -} - -do_test u8 -do_test i8 -do_test u16 -do_test i16 -do_test u32 -do_test i32 -do_test u64 -do_test i64 -do_test u128 -do_test i128 diff --git a/test/tests/autoinc2.sh b/test/tests/autoinc2.sh deleted file mode 100644 index 77fe9daf3a..0000000000 --- a/test/tests/autoinc2.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests unique constraints being violated during autoinc insertion" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -do_test() { - echo "RUNNING TEST FOR VALUE: $1" - reset_project - - cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use std::error::Error; -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -pub struct Person { - #[autoinc] - #[unique] - key_col: REPLACE_VALUE, - #[unique] - name: String, -} - -#[spacetimedb(reducer)] -pub fn add_new(name: String) -> Result<(), Box> { - let value = Person::insert(Person { key_col: 0, name })?; - println!("Assigned Value: {} -> {}", value.key_col, value.name); - Ok(()) -} - -#[spacetimedb(reducer)] -pub fn update(name: String, new_id: REPLACE_VALUE) { - Person::delete_by_name(&name); - let _value = Person::insert(Person { key_col: new_id, name }); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}:{}!", person.key_col, person.name); - } - println!("Hello, World!"); -} -EOF - - fsed "s/REPLACE_VALUE/$1/g" "${PROJECT_PATH}/src/lib.rs" - - run_test cargo run publish --project-path "$PROJECT_PATH" --clear-database - [ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] - IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - - run_test cargo run call "$IDENT" update Robert 2 - run_test cargo run call "$IDENT" add_new Success - if run_test cargo run call "$IDENT" add_new Failure ; then - # This add_new call should have failed. Its possible there was a duplicate insert - cargo run logs "$IDENT" - cargo run sql "$IDENT" 'SELECT * FROM Person' - exit 1 - fi - - run_test cargo run call "$IDENT" say_hello - run_test cargo run logs "$IDENT" 100 - [[ "$(grep 'Robert' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 2:Robert! ]] - [[ "$(grep 'Success' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ 1:Success! ]] - [[ "$(grep 'World' "$TEST_OUT" | tail -n 4)" =~ .*Hello,\ World! ]] - - clear_project -} - -do_test u8 -do_test i8 -do_test u16 -do_test i16 -do_test u32 -do_test i32 -do_test u64 -do_test i64 -do_test u128 -do_test i128 diff --git a/test/tests/bitcraftmini-pretest.sh b/test/tests/bitcraftmini-pretest.sh deleted file mode 100644 index 2e97c19586..0000000000 --- a/test/tests/bitcraftmini-pretest.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test performs all of the steps of the BitCraftMini getting started guide that I could automate quickly. Basically after this you just have to start up BitCraftMini and see if its working properly" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -[ -d ../BitCraftMini ] - -# 2. Compile the Spacetime Module -run_test cargo run publish -S --project-path "../BitCraftMini/Server" --clear-database -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" -sleep 2 -mkdir -p ../BitCraftMini/Client/Assets/_Project/autogen -run_test cargo run generate --out-dir ../BitCraftMini/Client/Assets/_Project/autogen --lang=cs --project-path "../BitCraftMini/Server" diff --git a/test/tests/cancel-reducer.sh b/test/tests/cancel-reducer.sh deleted file mode 100644 index 21456f2712..0000000000 --- a/test/tests/cancel-reducer.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Ensures cancelling a reducer works" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb, ScheduleToken}; - -#[spacetimedb(init)] -fn init() { - let token = spacetimedb::schedule!("100ms", reducer(1)); - token.cancel(); - let token = spacetimedb::schedule!("1000ms", reducer(2)); - spacetimedb::schedule!("500ms", do_cancel(token)); -} - -#[spacetimedb(reducer)] -fn do_cancel(token: ScheduleToken) { - token.cancel() -} - -#[spacetimedb(reducer)] -fn reducer(num: i32) { - println!("the reducer ran: {}", num) -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" -sleep 2 - -run_test cargo run logs "$ADDRESS" -! grep -c "the reducer ran" "$TEST_OUT" diff --git a/test/tests/connect-disconnect-from-cli.sh b/test/tests/connect-disconnect-from-cli.sh deleted file mode 100644 index 693eeab91d..0000000000 --- a/test/tests/connect-disconnect-from-cli.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This makes sure that the connect and disconnect functions are called when invoking a reducer from the CLI" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb, ReducerContext}; - -#[spacetimedb(connect)] -pub fn connected(_ctx: ReducerContext) { - println!("_connect called"); - panic!("Panic on connect"); -} - -#[spacetimedb(disconnect)] -pub fn disconnected(_ctx: ReducerContext) { - println!("disconnect called"); - panic!("Panic on disconnect"); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - println!("Hello, World!"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -run_test cargo run call "$IDENT" say_hello -run_test cargo run logs "$IDENT" -[ ' _connect called' == "$(grep '_connect called' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' disconnect called' == "$(grep 'disconnect called' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, World!' == "$(grep 'Hello, World!' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] diff --git a/test/tests/create-project-1.sh b/test/tests/create-project-1.sh deleted file mode 100644 index 113daffb26..0000000000 --- a/test/tests/create-project-1.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test just tests to see if the CLI is able to create a local project. This test does not depend on a running spacetimedb instance." - exit -fi - -set -euo pipefail - -source "./test/lib.include" - -run_fail_test cargo run init -run_fail_test cargo run init "$PROJECT_PATH" -rm -rf "$PROJECT_PATH" -mkdir -p "$PROJECT_PATH" -run_test cargo run init "$PROJECT_PATH" --lang rust diff --git a/test/tests/default-module-clippy.sh b/test/tests/default-module-clippy.sh deleted file mode 100644 index 57b0382da1..0000000000 --- a/test/tests/default-module-clippy.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests to make sure that the default rust module has no clippy errors or warnings" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cd "$PROJECT_PATH" -run_test cargo clippy -- -D warnings diff --git a/test/tests/describe.sh b/test/tests/describe.sh deleted file mode 100644 index 7f6e3930fd..0000000000 --- a/test/tests/describe.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests describing a module." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -pub struct Person { - name: String, -} - -#[spacetimedb(reducer)] -pub fn add(name: String) { - Person::insert(Person { name }); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -run_test cargo run describe "$IDENT" -run_test cargo run describe "$IDENT" reducer say_hello -run_test cargo run describe "$IDENT" table Person diff --git a/test/tests/detect-wasm-bindgen.sh b/test/tests/detect-wasm-bindgen.sh deleted file mode 100644 index b8d5c0c3c9..0000000000 --- a/test/tests/detect-wasm-bindgen.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests uploading a basic module and calling some functions and checking logs afterwards." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{log, spacetimedb}; - -#[spacetimedb(reducer)] -pub fn test() { - log::info!("Hello! {}", now()); -} - -#[wasm_bindgen::prelude::wasm_bindgen] -extern "C" { - fn now() -> i32; -} -EOF - -printf '\nwasm-bindgen = "0.2"\n' >> "${PROJECT_PATH}/Cargo.toml" - -run_fail_test cargo run build "${PROJECT_PATH}" - -[ $(grep "wasm-bindgen detected" "$TEST_OUT" | wc -l ) == 1 ] - diff --git a/test/tests/filtering.sh b/test/tests/filtering.sh deleted file mode 100644 index 1d0bbed82e..0000000000 --- a/test/tests/filtering.sh +++ /dev/null @@ -1,273 +0,0 @@ -#!/bin/bashtable - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests filtering reducers." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb, Identity}; - -#[spacetimedb(table)] -pub struct Person { - #[unique] - id: i32, - - name: String, - #[unique] - nick: String, -} - -#[spacetimedb(reducer)] -pub fn insert_person(id: i32, name: String, nick: String) { - Person::insert(Person { id, name, nick} ); -} - -#[spacetimedb(reducer)] -pub fn insert_person_twice(id: i32, name: String, nick: String) { - Person::insert(Person { id, name: name.clone(), nick: nick.clone()} ); - match Person::insert(Person { id, name: name.clone(), nick: nick.clone()}) { - Ok(_) => {}, - Err(_) => { - println!("UNIQUE CONSTRAINT VIOLATION ERROR: id {}: {}", id, name) - } - } -} - -#[spacetimedb(reducer)] -pub fn delete_person(id: i32) { - Person::delete_by_id(&id); -} - -#[spacetimedb(reducer)] -pub fn find_person(id: i32) { - match Person::filter_by_id(&id) { - Some(person) => println!("UNIQUE FOUND: id {}: {}", id, person.name), - None => println!("UNIQUE NOT FOUND: id {}", id), - } -} - -#[spacetimedb(reducer)] -pub fn find_person_by_name(name: String) { - for person in Person::filter_by_name(&name) { - println!("UNIQUE FOUND: id {}: {} aka {}", person.id, person.name, person.nick); - } -} - -#[spacetimedb(reducer)] -pub fn find_person_by_nick(nick: String) { - match Person::filter_by_nick(&nick) { - Some(person) => println!("UNIQUE FOUND: id {}: {}", person.id, person.nick), - None => println!("UNIQUE NOT FOUND: nick {}", nick), - } -} - -#[spacetimedb(table)] -#[spacetimedb(index(btree, name = "by_id", id))] -pub struct NonuniquePerson { - id: i32, - name: String, - is_human: bool, -} - -#[spacetimedb(reducer)] -pub fn insert_nonunique_person(id: i32, name: String, is_human: bool) { - NonuniquePerson::insert(NonuniquePerson { id, name, is_human } ); -} - -#[spacetimedb(reducer)] -pub fn find_nonunique_person(id: i32) { - for person in NonuniquePerson::filter_by_id(&id) { - println!("NONUNIQUE FOUND: id {}: {}", id, person.name) - } -} - -#[spacetimedb(reducer)] -pub fn find_nonunique_humans() { - for person in NonuniquePerson::filter_by_is_human(&true) { - println!("HUMAN FOUND: id {}: {}", person.id, person.name); - } -} - -#[spacetimedb(reducer)] -pub fn find_nonunique_non_humans() { - for person in NonuniquePerson::filter_by_is_human(&false) { - println!("NON-HUMAN FOUND: id {}: {}", person.id, person.name); - } -} - -// Ensure that [Identity] is filterable and a legal unique column. -#[spacetimedb(table)] -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(id_number: u64, name: String) { - let identity = identify(id_number); - IdentifiedPerson::insert(IdentifiedPerson { identity, name }); -} - -#[spacetimedb(reducer)] -fn find_identified_person(id_number: u64) { - let identity = identify(id_number); - match IdentifiedPerson::filter_by_identity(&identity) { - Some(person) => println!("IDENTIFIED FOUND: {}", person.name), - None => println!("IDENTIFIED NOT FOUND"), - } -} - -// Ensure that indices on non-unique columns behave as we expect. -#[spacetimedb(table)] -#[spacetimedb(index(btree, name="person_surname", surname))] -struct IndexedPerson { - #[unique] - id: i32, - given_name: String, - surname: String, -} - -#[spacetimedb(reducer)] -fn insert_indexed_person(id: i32, given_name: String, surname: String) { - IndexedPerson::insert(IndexedPerson { id, given_name, surname }); -} - -#[spacetimedb(reducer)] -fn delete_indexed_person(id: i32) { - IndexedPerson::delete_by_id(&id); -} - -#[spacetimedb(reducer)] -fn find_indexed_people(surname: String) { - for person in IndexedPerson::filter_by_surname(&surname) { - println!("INDEXED FOUND: id {}: {}, {}", person.id, person.surname, person.given_name); - } -} - -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -# Add some people. -run_test cargo run call "$IDENT" insert_person 23 Alice al -run_test cargo run call "$IDENT" insert_person 42 Bob bo -run_test cargo run call "$IDENT" insert_person 64 Bob b2 - -# Find a person who is there. -run_test cargo run call "$IDENT" find_person 23 -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE FOUND: id 23: Alice' == "$(grep 'UNIQUE FOUND: id 23' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Find persons with the same name. -run_test cargo run call "$IDENT" find_person_by_name Bob -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE FOUND: id 42: Bob aka bo' == "$(grep 'UNIQUE FOUND: id 42' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' UNIQUE FOUND: id 64: Bob aka b2' == "$(grep 'UNIQUE FOUND: id 64' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Fail to find a person who is not there. -run_test cargo run call "$IDENT" find_person 43 -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE NOT FOUND: id 43' == "$(grep 'UNIQUE NOT FOUND: id 43' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Find a person by nickname. -run_test cargo run call "$IDENT" find_person_by_nick al -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE FOUND: id 23: al' == "$(grep 'UNIQUE FOUND: id 23: al' "$TEST_OUT" | tail -n4 | cut -d: -f6-)" ] - -# Remove a person, and then fail to find them. -run_test cargo run call "$IDENT" delete_person 23 -run_test cargo run call "$IDENT" find_person 23 -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE NOT FOUND: id 23' == "$(grep 'UNIQUE NOT FOUND: id 23' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -# Also fail by nickname -run_test cargo run call "$IDENT" find_person_by_nick al -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE NOT FOUND: nick al' == "$(grep 'UNIQUE NOT FOUND: nick al' "$TEST_OUT" | tail -n4 | cut -d: -f6-)" ] - -# Add some nonunique people. -run_test cargo run call "$IDENT" insert_nonunique_person 23 Alice true -run_test cargo run call "$IDENT" insert_nonunique_person 42 Bob true - -# Find a nonunique person who is there. -run_test cargo run call "$IDENT" find_nonunique_person 23 -run_test cargo run logs "$IDENT" 100 -[ ' NONUNIQUE FOUND: id 23: Alice' == "$(grep 'NONUNIQUE FOUND: id 23' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Fail to find a nonunique person who is not there. -run_test cargo run call "$IDENT" find_nonunique_person 43 -run_test cargo run logs "$IDENT" 100 -[ '' == "$(grep 'NONUNIQUE NOT FOUND: id 43' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Insert a non-human, then find humans, then find non-humans -run_test cargo run call "$IDENT" insert_nonunique_person 64 Jibbitty false -run_test cargo run call "$IDENT" find_nonunique_humans -run_test cargo run logs "$IDENT" 100 -[ ' HUMAN FOUND: id 23: Alice' == "$(grep 'HUMAN FOUND: id 23' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' HUMAN FOUND: id 42: Bob' == "$(grep 'HUMAN FOUND: id 42' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -run_test cargo run call "$IDENT" find_nonunique_non_humans -run_test cargo run logs "$IDENT" 100 -[ ' NON-HUMAN FOUND: id 64: Jibbitty' == "$(grep 'NON-HUMAN FOUND: id 64' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Add another person with the same id, and find them both. -run_test cargo run call "$IDENT" insert_nonunique_person 23 Claire true -run_test cargo run call "$IDENT" find_nonunique_person 23 -run_test cargo run logs "$IDENT" 2 -[ ' NONUNIQUE FOUND: id 23: Alice' == "$(grep 'NONUNIQUE FOUND: id 23: Alice' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' NONUNIQUE FOUND: id 23: Claire' == "$(grep 'NONUNIQUE FOUND: id 23: Claire' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Check for issues with things present in index but not DB -run_test cargo run call "$IDENT" insert_person 101 Fee fee -run_test cargo run call "$IDENT" insert_person 102 Fi "fi" -run_test cargo run call "$IDENT" insert_person 103 Fo fo -run_test cargo run call "$IDENT" insert_person 104 Fum fum -run_test cargo run call "$IDENT" delete_person 103 -run_test cargo run call "$IDENT" find_person 104 -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE FOUND: id 104: Fum' == "$(grep 'UNIQUE FOUND: id 104: Fum' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# As above, but for non-unique indices: check for consistency between index and DB -run_test cargo run call "$IDENT" insert_indexed_person 7 James Bond -run_test cargo run call "$IDENT" insert_indexed_person 79 Gold Bond -run_test cargo run call "$IDENT" insert_indexed_person 1 Hydrogen Bond -run_test cargo run call "$IDENT" insert_indexed_person 100 Whiskey Bond -run_test cargo run call "$IDENT" delete_indexed_person 100 -run_test cargo run call "$IDENT" find_indexed_people Bond -run_test cargo run logs "$IDENT" 100 -[ 1 == "$(grep -c 'INDEXED FOUND: id 7: Bond, James' "$TEST_OUT")" ] -[ 1 == "$(grep -c 'INDEXED FOUND: id 79: Bond, Gold' "$TEST_OUT")" ] -[ 1 == "$(grep -c 'INDEXED FOUND: id 1: Bond, Hydrogen' "$TEST_OUT")" ] -[ 0 == "$(grep -c 'INDEXED FOUND: id 100: Bond, Whiskey' "$TEST_OUT")" ] - -# Non-unique version; does not work yet, see db_delete codegen in SpacetimeDB\crates\bindings-macro\src\lib.rs -# run_test cargo run call "$IDENT" insert_nonunique_person 101 Fee -# run_test cargo run call "$IDENT" insert_nonunique_person 102 "Fi" -# run_test cargo run call "$IDENT" insert_nonunique_person 103 Fo -# run_test cargo run call "$IDENT" insert_nonunique_person 104 Fum -# run_test cargo run call "$IDENT" find_nonunique_person 104 -# run_test cargo run logs "$IDENT" 100 -# [ ' NONUNIQUE FOUND: id 104: Fum' == "$(grep 'NONUNIQUE FOUND: id 104: Fum' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Filter by Identity -run_test cargo run call "$IDENT" insert_identified_person 23 Alice -run_test cargo run call "$IDENT" find_identified_person 23 -run_test cargo run logs "$IDENT" 100 -[ ' IDENTIFIED FOUND: Alice' == "$(grep 'IDENTIFIED FOUND: Alice' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] - -# Insert row with unique columns twice should fail -run_test cargo run call "$IDENT" insert_person_twice 23 Alice al -run_test cargo run logs "$IDENT" 100 -[ ' UNIQUE CONSTRAINT VIOLATION ERROR: id 23: Alice' == "$(grep 'UNIQUE CONSTRAINT VIOLATION ERROR: id 23: Alice' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] diff --git a/test/tests/identity-import-test.sh b/test/tests/identity-import-test.sh deleted file mode 100644 index aaf6547e7e..0000000000 --- a/test/tests/identity-import-test.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test tries to import a known good identity to our local ~/.spacetime/config.toml file. This test does not require a remote spacetimedb instance." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity token "$IDENT" -TOKEN=$(cat "$TEST_OUT") - -reset_config - -# Fetch the server's fingerprint. -# The fingerprint is required for `identity list`. -run_test cargo run server fingerprint localhost -f - -run_test cargo run identity import "$IDENT" "$TOKEN" -run_test cargo run identity list -exit 0 -[ "$(grep "$IDENT" "$TEST_OUT" | awk '{print $1}')" == '***' ] diff --git a/test/tests/identity-new-email.sh b/test/tests/identity-new-email.sh deleted file mode 100644 index 25963903f5..0000000000 --- a/test/tests/identity-new-email.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test is designed to make sure an email can be set while creating a new identity" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -# Create a new identity -EMAIL="$(random_string)@clockworklabs.io" -run_test cargo run identity new --email "$EMAIL" -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -TOKEN="$(cargo run identity token "$IDENT")" - -# Reset our config so we lose this identity -reset_config - -# Import this identity, and set it as the default identity -run_test cargo run identity import "$IDENT" "$TOKEN" -run_test cargo run identity set-default "$IDENT" - -# Configure our email -run_test cargo run identity set-email "$IDENT" "$EMAIL" -[ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] - -# Reset config again -reset_config - -# Find our identity by its email -run_test cargo run identity find "$EMAIL" -[ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] diff --git a/test/tests/identity-remove-1.sh b/test/tests/identity-remove-1.sh deleted file mode 100644 index 8c83cb6fc4..0000000000 --- a/test/tests/identity-remove-1.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test checks to see if you're able to delete an identity from your local ~/.spacetime/config.toml file." - exit -fi - -set -euox pipefail -set -x - -source "./test/lib.include" - -# Fetch the server's fingerprint. -# The fingerprint is required for `identity list`. -run_test cargo run server fingerprint localhost -f - -run_test cargo run identity new --no-email -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity list -[ "1" == "$(grep -c "$IDENT" "$TEST_OUT")" ] - -run_test cargo run identity remove "$IDENT" -run_test cargo run identity list -[ "0" == "$(grep -c "$IDENT" "$TEST_OUT")" ] - -run_fail_test cargo run identity remove "$IDENT" diff --git a/test/tests/identity-remove-2.sh b/test/tests/identity-remove-2.sh deleted file mode 100644 index b65f9ab327..0000000000 --- a/test/tests/identity-remove-2.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test checks to see if you're able to delete all identities with --force" - exit -fi - -set -euox pipefail -set -x - -source "./test/lib.include" - -# Fetch the server's fingerprint. -# The fingerprint is required for `identity list`. -run_test cargo run server fingerprint localhost -f - -run_test cargo run identity new --no-email -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity list -[ "1" == "$(grep -c "$IDENT" "$TEST_OUT")" ] - -run_test cargo run identity remove "$IDENT" -run_test cargo run identity list -[ "0" == "$(grep -c "$IDENT" "$TEST_OUT")" ] - -run_test cargo run identity remove --all --force diff --git a/test/tests/identity-set-default.sh b/test/tests/identity-set-default.sh deleted file mode 100644 index 10fe17f861..0000000000 --- a/test/tests/identity-set-default.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test makes sure that we are able to set a default identity." - exit -fi - -set -euox pipefail -set -x - -source "./test/lib.include" - -# Fetch the server's fingerprint. -# The fingerprint is required for `identity list`. -run_test cargo run server fingerprint localhost -f - -run_test cargo run identity new --no-email -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity list -[ "0" == "$(grep -F "***" "$TEST_OUT" | grep -c "$IDENT")" ] -run_test cargo run identity set-default "$IDENT" - -run_test cargo run identity list -[ "1" == "$(grep -F "***" "$TEST_OUT" | grep -c "$IDENT")" ] diff --git a/test/tests/identity-set-email.sh b/test/tests/identity-set-email.sh deleted file mode 100644 index 755d3a2aac..0000000000 --- a/test/tests/identity-set-email.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo 'This test is designed to test the identity set-email functionality' - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -# Create a new identity -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -EMAIL="$(random_string)@clockworklabs.io" -TOKEN="$(cargo run identity token "$IDENT")" - -# Reset our config so we lose this identity -reset_config - -# Import this identity, and set it as the default identity -run_test cargo run identity import "$IDENT" "$TOKEN" -run_test cargo run identity set-default "$IDENT" - -# Configure our email -run_test cargo run identity set-email "$IDENT" "$EMAIL" -[ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] - -# Reset config again -reset_config - -# Find our identity by its email -run_test cargo run identity find "$EMAIL" -[ "$IDENT" == "$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" ] -[ "$EMAIL" == "$(grep EMAIL "$TEST_OUT" | awk '{print $2}')" ] diff --git a/test/tests/module-nested-op.sh b/test/tests/module-nested-op.sh deleted file mode 100644 index d4402b031f..0000000000 --- a/test/tests/module-nested-op.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests uploading a basic module and calling some functions and checking logs afterwards." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -pub struct Account { - name: String, - #[unique] - id: i32, -} - -#[spacetimedb(table)] -pub struct Friends { - friend_1: i32, - friend_2: i32, -} - -#[spacetimedb(reducer)] -pub fn create_account(account_id: i32, name: String) { - Account::insert(Account { id: account_id, name } ); -} - -#[spacetimedb(reducer)] -pub fn add_friend(my_id: i32, their_id: i32) { - - // Make sure our friend exists - for account in Account::iter() { - if account.id == their_id { - Friends::insert(Friends { friend_1: my_id, friend_2: their_id } ); - return; - } - } -} - -#[spacetimedb(reducer)] -pub fn say_friends() { - for friendship in Friends::iter() { - let friend1 = Account::filter_by_id(&friendship.friend_1).unwrap(); - let friend2 = Account::filter_by_id(&friendship.friend_2).unwrap(); - println!("{} is friends with {}", friend1.name, friend2.name); - } -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -run_test cargo run call "$IDENT" create_account 1 House -run_test cargo run call "$IDENT" create_account 2 Wilson -run_test cargo run call "$IDENT" add_friend 1 2 -run_test cargo run call "$IDENT" say_friends -run_test cargo run logs "$IDENT" 100 -[ ' House is friends with Wilson' == "$(grep 'House' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] diff --git a/test/tests/namespace-custom-cs.sh b/test/tests/namespace-custom-cs.sh deleted file mode 100644 index 082ed16fe2..0000000000 --- a/test/tests/namespace-custom-cs.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test tests to sure that when a custom namespace is specified on the command line, it actually gets used in generation" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -TMP_DIR=$(mktemp -d) -NAMESPACE=$(random_string) - -run_test cargo run generate --out-dir "${TMP_DIR}" --lang cs --namespace "${NAMESPACE}" --project-path "${PROJECT_PATH}" - -LINES="$(grep -r -o "namespace ${NAMESPACE}" "${TMP_DIR}" | wc -l | tr -d ' ')" -if [ "${LINES}" != 5 ] ; then - echo "FOUND: ${LINES} EXPECTED: " - exit 1 -fi - -LINES="$(grep -r -o "using SpacetimeDB;" "${TMP_DIR}" | wc -l | tr -d ' ')" -if [ "${LINES}" != 5 ] ; then - echo "FOUND: ${LINES} EXPECTED: " - exit 1 -fi - -rm -rf "${TMP_DIR}" diff --git a/test/tests/namespace-spacetimedb-cs.sh b/test/tests/namespace-spacetimedb-cs.sh deleted file mode 100644 index c07b9e2e2d..0000000000 --- a/test/tests/namespace-spacetimedb-cs.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test tests to make sure that the default namespace is working properly" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -TMP_DIR=$(mktemp -d) -NAMESPACE=SpacetimeDB.Types - -run_test cargo run generate --out-dir "${TMP_DIR}" --lang cs --project-path "${PROJECT_PATH}" - -LINES="$(grep -r -o "namespace ${NAMESPACE}" "${TMP_DIR}" | wc -l | tr -d ' ')" -if [ "${LINES}" != 5 ] ; then - echo "FOUND: ${LINES} EXPECTED: " - exit 1 -fi - -echo "${TMP_DIR}" -#rm -rf "${TMP_DIR}" diff --git a/test/tests/new-user-flow.sh b/test/tests/new-user-flow.sh deleted file mode 100644 index 1a1d38f69c..0000000000 --- a/test/tests/new-user-flow.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test is designed to test the entirety of the new user flow." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cargo run identity new --no-email - -## Write a spacetimedb rust module -cat > "${PROJECT_PATH}/src/lib.rs" <1{print $NF}')" - -# We have to give the database some time to setup our instance -sleep 2 - -# Calling our database -run_test cargo run call "$ADDRESS" say_hello -run_test cargo run logs "$ADDRESS" -if [ "$(grep -c "Hello, World!" "$TEST_OUT")" != 1 ]; then exit 1; fi - -## Calling functions with arguments -run_test cargo run call "$ADDRESS" add Tyler -run_test cargo run call "$ADDRESS" say_hello -run_test cargo run logs "$ADDRESS" - -[ "$(grep -c "Hello, World!" "$TEST_OUT")" == 2 ] -[ "$(grep -c "Hello, Tyler!" "$TEST_OUT")" == 1 ] - -run_test cargo run sql "$ADDRESS" "SELECT * FROM Person" -[ "$(tail -n 3 "$TEST_OUT")" == \ -' name '$'\n'\ -'-------'$'\n'\ -' Tyler ' ] diff --git a/test/tests/panic.sh b/test/tests/panic.sh deleted file mode 100644 index 130af2d97d..0000000000 --- a/test/tests/panic.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Tests to check if a SpacetimeDB module can handle a panic" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{spacetimedb, println}; -use std::cell::RefCell; - -thread_local! { - static X: RefCell = RefCell::new(0); -} -#[spacetimedb(reducer)] -fn first() { - X.with(|x| { - let x = x.borrow_mut(); - panic!() - }) -} -#[spacetimedb(reducer)] -fn second() { - X.with(|x| *x.borrow_mut()); - println!("Test Passed"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -set +e -cargo run call "$IDENT" first -set -e -run_test cargo run call "$IDENT" second - -run_test cargo run logs "$IDENT" -[ ' Test Passed' == "$(grep 'Test Passed' "$TEST_OUT" | cut -d: -f6-)" ] diff --git a/test/tests/permissions-call.sh b/test/tests/permissions-call.sh deleted file mode 100644 index a9a37ce19d..0000000000 --- a/test/tests/permissions-call.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test makes sure that anyone has the permission to call any standard reducer" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -TOKEN="$(cargo run identity token "$IDENT")" -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -sleep 2 -DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -reset_config -run_test cargo run identity new --no-email -run_test cargo run call "$DATABASE" "say_hello" - -reset_config -run_test cargo run identity import "$IDENT" "$TOKEN" -run_test cargo run identity set-default "$IDENT" -run_test cargo run logs "$DATABASE" 10000 -if [ "1" != "$(grep -c "World" "$TEST_OUT")" ]; then exit 1; fi diff --git a/test/tests/permissions-delete.sh b/test/tests/permissions-delete.sh deleted file mode 100644 index 00bd6556e2..0000000000 --- a/test/tests/permissions-delete.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test checks to make sure that you cannot delete a database that you do not own." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity set-default "$IDENT" -run_test cargo run publish --skip_clippy --project-path="$PROJECT_PATH" --clear-database -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -reset_config -if cargo run delete "$ADDRESS"; then exit 1; fi diff --git a/test/tests/permissions-describe.sh b/test/tests/permissions-describe.sh deleted file mode 100644 index 7d565017b8..0000000000 --- a/test/tests/permissions-describe.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test makes sure that anyone can describe any database." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -sleep 2 -DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -reset_config -run_test cargo run identity new --no-email - -# It is expected that you should be able to describe any database even if you -# do not own it. -if ! run_test cargo run describe "$DATABASE" ; then exit 1; fi diff --git a/test/tests/permissions-logs.sh b/test/tests/permissions-logs.sh deleted file mode 100644 index a9f8f416d2..0000000000 --- a/test/tests/permissions-logs.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test makes sure that we are not able to view the logs of a module that we don't have permission to view." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -sleep 2 -DATABASE="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -reset_config -run_test cargo run identity new --no-email -run_test cargo run call "$DATABASE" "say_hello" - -reset_config -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity set-default "$IDENT" -if run_test cargo run logs "$DATABASE" 10000 ; then exit 1; fi -if [ "0" != "$(grep -c "World" "$TEST_OUT")" ]; then exit 1; fi diff --git a/test/tests/permissions-private-table.sh b/test/tests/permissions-private-table.sh deleted file mode 100644 index 005cdfadfe..0000000000 --- a/test/tests/permissions-private-table.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Tests that a private table can only be queried by the database owner" - exit -fi - -set -eoux pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" <1{print $NF}')" - -run_test cargo run sql "$DATABASE" 'select * from _Secret' -result="$(tail -n 3 "$TEST_OUT")" -[ "${result//[$'\n\r\t ']}" == "answer--------42" ] - -reset_config -run_test cargo run identity new --no-email -IDENT="$(grep IDENTITY "$TEST_OUT" | awk '{print $2}')" -run_test cargo run identity set-default "$IDENT" - -if cargo run sql "$DATABASE" 'select * from _Secret'; then exit 1; fi diff --git a/test/tests/permissions-publish.sh b/test/tests/permissions-publish.sh deleted file mode 100644 index fb928de6f5..0000000000 --- a/test/tests/permissions-publish.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test checks to make sure that you cannot publish to an address that you do not own." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run identity set-default "$IDENT" -run_test cargo run publish --skip_clippy --project-path="$PROJECT_PATH" --clear-database -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -reset_config -if cargo run publish --skip_clippy "$ADDRESS" --project-path="$PROJECT_PATH" --clear-database ; then exit 1; fi diff --git a/test/tests/register-domain.sh b/test/tests/register-domain.sh deleted file mode 100644 index 9f4c63480a..0000000000 --- a/test/tests/register-domain.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Attempts to register some valid domains and makes sure invalid domains cannot be registered." - exit -fi - -set -euox pipefail - -source "./test/lib.include" -RAND_DOMAIN=$(random_string) - - -run_test cargo run identity new --no-email -IDENT=$(grep IDENTITY "$TEST_OUT" | awk '{print $2}') -run_test cargo run dns register-tld "$RAND_DOMAIN" -clear_project -reset_project -run_test cargo run publish --skip_clippy "$RAND_DOMAIN" --project-path "$PROJECT_PATH" --clear-database -run_test cargo run publish --skip_clippy "$RAND_DOMAIN/test" --project-path "$PROJECT_PATH" --clear-database -run_test cargo run publish --skip_clippy "$RAND_DOMAIN/test/test2" --project-path "$PROJECT_PATH" --clear-database - -run_fail_test cargo run publish --skip_clippy "$RAND_DOMAIN//test" --project-path "$PROJECT_PATH" --clear-database -run_fail_test cargo run publish --skip_clippy "$RAND_DOMAIN/test/" --project-path "$PROJECT_PATH" --clear-database -run_fail_test cargo run publish --skip_clippy "$RAND_DOMAIN/test//test2" --project-path "$PROJECT_PATH" --clear-database diff --git a/test/tests/reverse-dns.sh b/test/tests/reverse-dns.sh deleted file mode 100644 index 9e641f32ab..0000000000 --- a/test/tests/reverse-dns.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests the functionality of spacetime reverse dns lookups." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -RAND=$(random_string) -run_test cargo run dns register-tld "$RAND" -run_test cargo run publish --skip_clippy "$RAND" --project-path "$PROJECT_PATH" --clear-database -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" -if [ "$ADDRESS" == "" ] ; then - exit 1 -fi - -run_test cargo run dns reverse-lookup "$ADDRESS" -if [ "$RAND" != "$(tail -n 1 $TEST_OUT)" ] ; then - exit 1 -fi diff --git a/test/tests/setname.sh b/test/tests/setname.sh deleted file mode 100644 index 66cf553db7..0000000000 --- a/test/tests/setname.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Tests the functionality of the setname command." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run identity init-default -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -RAND_NAME="$(random_string)" -run_test cargo run dns register-tld "$RAND_NAME" -run_test cargo run dns set-name "$RAND_NAME" "$ADDRESS" -run_test cargo run dns lookup "$RAND_NAME" -[ "$(cat "$TEST_OUT" | tail -n 1)" == "$ADDRESS" ] - -run_test cargo run dns reverse-lookup "$ADDRESS" -[ "$(cat "$TEST_OUT" | tail -n 1)" == "$RAND_NAME" ] diff --git a/test/tests/spacetime-server.sh b/test/tests/spacetime-server.sh deleted file mode 100644 index 7901ade3f0..0000000000 --- a/test/tests/spacetime-server.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "Verify that we can add and list server configurations" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -run_test cargo run server add "https://testnet.spacetimedb.com" testnet --no-fingerprint -[ "$(grep Host "$TEST_OUT")" == "Host: testnet.spacetimedb.com" ] -[ "$(grep Protocol "$TEST_OUT")" == "Protocol: https" ] - -run_test cargo run server list -[[ "$(grep testnet.spacetimedb.com "$TEST_OUT")" =~ [[:space:]]*testnet\.spacetimedb\.com[[:space:]]+https[[:space:]]+testnet[[:space:]]* ]] -[[ "$(grep 127.0.0.1:3000 "$TEST_OUT")" =~ [[:space:]]*\*\*\*[[:space:]]+127\.0\.0\.1:3000[[:space:]]+http[[:space:]]* ]] - -run_test cargo run server fingerprint 127.0.0.1:3000 -f -grep "No saved fingerprint for server 127.0.0.1:3000." "$TEST_OUT" - -run_test cargo run server fingerprint 127.0.0.1:3000 -grep "Fingerprint is unchanged for server 127.0.0.1:3000" "$TEST_OUT" - -run_test cargo run server fingerprint localhost -grep "Fingerprint is unchanged for server localhost" "$TEST_OUT" diff --git a/test/tests/upload-module-1.sh b/test/tests/upload-module-1.sh deleted file mode 100644 index e48dd79dfc..0000000000 --- a/test/tests/upload-module-1.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests uploading a basic module and calling some functions and checking logs afterwards." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -pub struct Person { - name: String, -} - -#[spacetimedb(reducer)] -pub fn add(name: String) { - Person::insert(Person { name }); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -run_test cargo run call "$IDENT" add Robert -run_test cargo run call "$IDENT" add Julie -run_test cargo run call "$IDENT" add Samantha -run_test cargo run call "$IDENT" say_hello -run_test cargo run logs "$IDENT" 100 -[ ' Hello, Samantha!' == "$(grep 'Samantha' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, Julie!' == "$(grep 'Julie' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, Robert!' == "$(grep 'Robert' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, World!' == "$(grep 'World' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] diff --git a/test/tests/upload-module-2.sh b/test/tests/upload-module-2.sh deleted file mode 100644 index ab3bb29a73..0000000000 --- a/test/tests/upload-module-2.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This test deploys a module with a repeating reducer and checks the logs to make sure its running." - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb, Timestamp}; - -#[spacetimedb(init)] -fn init() { - spacetimedb::schedule!("100ms", my_repeating_reducer(Timestamp::now())); -} - -#[spacetimedb(reducer, repeat = 100ms)] -pub fn my_repeating_reducer(prev: Timestamp) { - println!("Invoked: ts={:?}, delta={:?}", Timestamp::now(), prev.elapsed()); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -ADDRESS="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" -sleep 2 - -run_test cargo run logs "$ADDRESS" -LINES="$(grep -c "Invoked" "$TEST_OUT")" - -sleep 4 -run_test cargo run logs "$ADDRESS" -LINES_NEW="$(grep -c "Invoked" "$TEST_OUT")" -((LINES < LINES_NEW)) diff --git a/test/tests/zz_docker-restart-module.sh b/test/tests/zz_docker-restart-module.sh deleted file mode 100644 index 5608908d09..0000000000 --- a/test/tests/zz_docker-restart-module.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests to see if a SpacetimeDB module's reducer can still be invoked after a restart" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -# Note: creating indexes on `Person` -# exercises more possible failure cases when replaying after restart -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -#[spacetimedb(index(btree, name = "name_idx", name))] -pub struct Person { - #[primarykey] - #[autoinc] - id: u32, - name: String, -} - -#[spacetimedb(reducer)] -pub fn add(name: String) { -Person::insert(Person { id: 0, name }).unwrap(); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" -run_test cargo run call "$IDENT" add Robert - -restart_docker -run_test cargo run call "$IDENT" add Julie -run_test cargo run call "$IDENT" add Samantha -run_test cargo run call "$IDENT" say_hello -run_test cargo run logs "$IDENT" 100 - -[ ' Hello, Samantha!' == "$(grep 'Samantha' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, Julie!' == "$(grep 'Julie' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, Robert!' == "$(grep 'Robert' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] -[ ' Hello, World!' == "$(grep 'World' "$TEST_OUT" | tail -n 4 | cut -d: -f6-)" ] diff --git a/test/tests/zz_docker-restart-repeating-reducer.sh b/test/tests/zz_docker-restart-repeating-reducer.sh deleted file mode 100644 index cf216562d3..0000000000 --- a/test/tests/zz_docker-restart-repeating-reducer.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests to see if a SpacetimeDB module's repeating reducers are rescheduled after a restart" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb, Timestamp}; - -#[spacetimedb(init)] -fn init() { - spacetimedb::schedule!("100ms", my_repeating_reducer(Timestamp::now())); -} - -#[spacetimedb(reducer, repeat = 100ms)] -pub fn my_repeating_reducer(prev: Timestamp) { - println!("Invoked: ts={:?}, delta={:?}", Timestamp::now(), prev.elapsed()); -} - -#[spacetimedb(reducer)] -pub fn dummy() {} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -restart_docker -run_test cargo run call "$IDENT" dummy -sleep 4 - -run_test cargo run logs "$IDENT" -LINES="$(grep -c "Invoked" "$TEST_OUT")" - -sleep 4 -run_test cargo run logs "$IDENT" -LINES_NEW="$(grep -c "Invoked" "$TEST_OUT")" -((LINES < LINES_NEW)) diff --git a/test/tests/zz_docker-restart-sql.sh b/test/tests/zz_docker-restart-sql.sh deleted file mode 100644 index 826e6e17b3..0000000000 --- a/test/tests/zz_docker-restart-sql.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -if [ "$DESCRIBE_TEST" = 1 ] ; then - echo "This tests to see if SpacetimeDB can be queried after a restart" - exit -fi - -set -euox pipefail - -source "./test/lib.include" - -# Note: creating indexes on `Person` -# exercises more possible failure cases when replaying after restart -cat > "${PROJECT_PATH}/src/lib.rs" << EOF -use spacetimedb::{println, spacetimedb}; - -#[spacetimedb(table)] -#[spacetimedb(index(btree, name = "name_idx", name))] -pub struct Person { - #[primarykey] - #[autoinc] - id: u32, - name: String, -} - -#[spacetimedb(reducer)] -pub fn add(name: String) { -Person::insert(Person { id: 0, name }).unwrap(); -} - -#[spacetimedb(reducer)] -pub fn say_hello() { - for person in Person::iter() { - println!("Hello, {}!", person.name); - } - println!("Hello, World!"); -} -EOF - -run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH" --clear-database -[ "1" == "$(grep -c "reated new database" "$TEST_OUT")" ] -IDENT="$(grep "reated new database" "$TEST_OUT" | awk 'NF>1{print $NF}')" - -run_test cargo run call "$IDENT" add Robert -run_test cargo run call "$IDENT" add Julie -run_test cargo run call "$IDENT" add Samantha -run_test cargo run call "$IDENT" say_hello -run_test cargo run logs "$IDENT" 100 - -[ ' Hello, Samantha!' == "$(tail -n 4 "$TEST_OUT" | grep 'Samantha' | cut -d: -f6-)" ] -[ ' Hello, Julie!' == "$(tail -n 4 "$TEST_OUT" | grep 'Julie' | cut -d: -f6-)" ] -[ ' Hello, Robert!' == "$(tail -n 4 "$TEST_OUT" | grep 'Robert' | cut -d: -f6-)" ] -[ ' Hello, World!' == "$(tail -n 4 "$TEST_OUT" | grep 'World' | cut -d: -f6-)" ] - -restart_docker -run_test cargo run sql "${IDENT}" "SELECT name FROM Person WHERE id = 3" -[ 'Samantha' == "$(tail -n 1 "$TEST_OUT" | grep 'Samantha' | awk '{$1=$1};1')" ]