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:
Noa
2024-03-13 21:47:38 -05:00
committed by GitHub
parent f1226a056c
commit 540c519002
73 changed files with 2084 additions and 2060 deletions
+2 -2
View File
@@ -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
+3
View File
@@ -192,6 +192,9 @@ linux-target/
/target
#Cargo.lock
__pycache__/
.test_out
## JetBrains
.idea/
+25 -43
View File
@@ -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> {
+19 -2
View File
@@ -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
+2
View File
@@ -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,
+1 -1
View File
@@ -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,
+8 -15
View File
@@ -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)?;
+12 -5
View File
@@ -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> {
+1 -1
View File
@@ -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);
}
}
+214
View File
@@ -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
+96
View File
@@ -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()
+6
View File
@@ -0,0 +1,6 @@
default_server = "127.0.0.1:3000"
[[server_configs]]
nickname = "localhost"
host = "127.0.0.1:3000"
protocol = "http"
View File
+117
View File
@@ -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)
+33
View File
@@ -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)
+18
View File
@@ -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)
+12
View File
@@ -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)
+31
View File
@@ -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")
+25
View File
@@ -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)
+50
View File
@@ -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)
+250
View File
@@ -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))
+137
View File
@@ -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())
+53
View File
@@ -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))
+162
View File
@@ -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)
+35
View File
@@ -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)
+51
View File
@@ -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
""")
+32
View File
@@ -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))
+105
View File
@@ -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")
+25
View File
@@ -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)
""")
+135
View File
@@ -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
""")
+376
View File
@@ -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
-7
View File
@@ -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'
-119
View File
@@ -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
-329
View File
@@ -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
-69
View File
@@ -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
-84
View File
@@ -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
-19
View File
@@ -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"
-40
View File
@@ -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"
-41
View File
@@ -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-)" ]
-16
View File
@@ -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
-13
View File
@@ -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
-40
View File
@@ -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
-31
View File
@@ -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 ]
-273
View File
@@ -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-)" ]
-26
View File
@@ -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}')" == '***' ]
-36
View File
@@ -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}')" ]
-27
View File
@@ -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"
-27
View File
@@ -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
-25
View File
@@ -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")" ]
-36
View File
@@ -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}')" ]
-64
View File
@@ -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-)" ]
-29
View File
@@ -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}"
-24
View File
@@ -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}"
-61
View File
@@ -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 ' ]
-43
View File
@@ -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-)" ]
-27
View File
@@ -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
-19
View File
@@ -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
-22
View File
@@ -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
-26
View File
@@ -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
-39
View File
@@ -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
-19
View File
@@ -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
-25
View File
@@ -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
-23
View File
@@ -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
-23
View File
@@ -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" ]
-27
View File
@@ -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"
-46
View File
@@ -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-)" ]
-37
View File
@@ -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))
-54
View File
@@ -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))
-57
View File
@@ -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')" ]