mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-26 20:11:43 -04:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -192,6 +192,9 @@ linux-target/
|
||||
/target
|
||||
#Cargo.lock
|
||||
|
||||
__pycache__/
|
||||
.test_out
|
||||
|
||||
## JetBrains
|
||||
.idea/
|
||||
|
||||
|
||||
+25
-43
@@ -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<PathBuf> {
|
||||
[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<RawConfig> {
|
||||
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> {
|
||||
|
||||
@@ -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::<u32>("num_lines").copied();
|
||||
let database = args.get_one::<String>("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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -63,21 +63,14 @@ pub async fn get_identity<S: ControlStateDelegate>(
|
||||
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::<GetIdentityResponseEntry>::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::<Vec<_>>();
|
||||
(!identities.is_empty()).then_some(GetIdentityResponse { identities })
|
||||
}
|
||||
};
|
||||
let identity_response = lookup.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
@@ -107,10 +107,11 @@ impl<'de, D: serde::Deserializer<'de>> Deserializer<'de> for SerdeDeserializer<D
|
||||
|
||||
fn deserialize_bytes<V: super::SliceVisitor<'de, [u8]>>(self, visitor: V) -> Result<V::Output, Self::Error> {
|
||||
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<V: super::ArrayVisitor<'de, T::Output>, T: super::DeserializeSeed<'de> + Clone>(
|
||||
@@ -466,16 +467,22 @@ impl<'de, V: super::SliceVisitor<'de, str>> serde::Visitor<'de> for StrVisitor<V
|
||||
|
||||
/// Translates a `SliceVisitor<'de, str>` to `serde::Visitor<'de>`
|
||||
/// for implementing `deserialize_bytes`.
|
||||
struct BytesVisitor<V> {
|
||||
struct BytesVisitor<V, const HUMAN_READABLE: bool> {
|
||||
/// The `SliceVisitor<'de, [u8]>`.
|
||||
visitor: V,
|
||||
}
|
||||
|
||||
impl<'de, V: super::SliceVisitor<'de, [u8]>> serde::Visitor<'de> for BytesVisitor<V> {
|
||||
impl<'de, V: super::SliceVisitor<'de, [u8]>, const HUMAN_READABLE: bool> serde::Visitor<'de>
|
||||
for BytesVisitor<V, HUMAN_READABLE>
|
||||
{
|
||||
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<E: serde::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
default_server = "127.0.0.1:3000"
|
||||
|
||||
[[server_configs]]
|
||||
nickname = "localhost"
|
||||
host = "127.0.0.1:3000"
|
||||
protocol = "http"
|
||||
@@ -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<dyn Error>> {
|
||||
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)
|
||||
|
||||
@@ -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<reducer>) {
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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<u32> = 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))
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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" <<EOF
|
||||
class SqlFormat(Smoketest):
|
||||
MODULE_CODE = """
|
||||
use spacetimedb::{spacetimedb, Identity};
|
||||
|
||||
#[derive(spacetimedb::SpacetimeType)]
|
||||
@@ -88,19 +80,15 @@ pub fn test() {
|
||||
},
|
||||
});
|
||||
}
|
||||
EOF
|
||||
"""
|
||||
def test_sql_format(self):
|
||||
"""This test is designed to test the format of the output of sql queries"""
|
||||
|
||||
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}')"
|
||||
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)
|
||||
""")
|
||||
@@ -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
|
||||
""")
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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 <test-name>"
|
||||
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 <test-name> <test-out-file>"
|
||||
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 <test-name> <result-code> <project-path> <out-file-path> <config-file-path>"
|
||||
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<length; i++)) ; do
|
||||
pid=${TESTS_PID[$i]}
|
||||
if [ "$pid" == "" ] ; then
|
||||
continue
|
||||
fi
|
||||
FOUND=1
|
||||
|
||||
# If the process is still running, skip it
|
||||
if kill -0 "$pid" 2>/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
|
||||
@@ -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
|
||||
@@ -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<dyn Error>> {
|
||||
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
|
||||
@@ -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"
|
||||
@@ -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<reducer>) {
|
||||
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"
|
||||
@@ -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-)" ]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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-)" ]
|
||||
@@ -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}')" == '***' ]
|
||||
@@ -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}')" ]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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")" ]
|
||||
@@ -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}')" ]
|
||||
@@ -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-)" ]
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
@@ -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" <<EOF
|
||||
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!");
|
||||
}
|
||||
EOF
|
||||
|
||||
## Publish your module
|
||||
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}')"
|
||||
|
||||
# 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 ' ]
|
||||
@@ -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<u32> = 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-)" ]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" <<EOF
|
||||
use spacetimedb::spacetimedb;
|
||||
|
||||
#[spacetimedb(table)]
|
||||
#[sats(name = "_Secret")]
|
||||
pub struct Secret {
|
||||
answer: u8,
|
||||
}
|
||||
|
||||
#[spacetimedb(init)]
|
||||
pub fn init() {
|
||||
Secret::insert(Secret { answer: 42 });
|
||||
}
|
||||
EOF
|
||||
|
||||
run_test cargo run publish --skip_clippy --project-path "$PROJECT_PATH"
|
||||
DATABASE="$(grep "Created new database" "$TEST_OUT" | awk 'NF>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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" ]
|
||||
@@ -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"
|
||||
@@ -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-)" ]
|
||||
@@ -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))
|
||||
@@ -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-)" ]
|
||||
@@ -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))
|
||||
@@ -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')" ]
|
||||
Reference in New Issue
Block a user