Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

# nodejs and pnpm are required for the typescript quickstart smoketest
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18

- uses: pnpm/action-setup@v4
with:
run_install: true

- name: Install psql (Windows)
if: runner.os == 'Windows'
run: choco install psql -y --no-progress
Expand Down
17 changes: 17 additions & 0 deletions smoketests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import logging
import http.client
import tomllib
import functools

# miscellaneous file paths
TEST_DIR = Path(__file__).parent
Expand All @@ -28,6 +29,7 @@
TEMPLATE_CARGO_TOML = open(STDB_DIR / "crates/cli/templates/basic-rust/server/Cargo.toml").read()
bindings_path = (STDB_DIR / "crates/bindings").absolute()
escaped_bindings_path = str(bindings_path).replace('\\', '\\\\\\\\') # double escape for re.sub + toml
TYPESCRIPT_BINDINGS_PATH = (STDB_DIR / "crates/bindings-typescript").absolute()
TEMPLATE_CARGO_TOML = (re.compile(r"^spacetimedb\s*=.*$", re.M) \
.sub(f'spacetimedb = {{ path = "{escaped_bindings_path}", features = {{features}} }}', TEMPLATE_CARGO_TOML))

Expand Down Expand Up @@ -170,6 +172,21 @@ def run_cmd(*args, capture_stderr=True, check=True, full_output=False, cmd_name=
output.check_returncode()
return output if full_output else output.stdout

@functools.cache
def pnpm_path():
pnpm = shutil.which("pnpm")
if not pnpm:
raise Exception("pnpm not installed")
return pnpm

def pnpm(*args, **kwargs):
return run_cmd(pnpm_path(), *args, **kwargs)

@functools.cache
def build_typescript_sdk():
pnpm("install", cwd=TYPESCRIPT_BINDINGS_PATH)
pnpm("build", cwd=TYPESCRIPT_BINDINGS_PATH)

def spacetime(*args, **kwargs):
return run_cmd(SPACETIME_BIN, *args, cmd_name="spacetime", **kwargs)

Expand Down
10 changes: 7 additions & 3 deletions smoketests/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ def restart_docker():

# Ensure all nodes are running.
attempts = 0
while attempts < 5:
while attempts < 10:
attempts += 1
if all(container.is_running(docker, spacetimedb_ping_url) for container in containers):
containers_alive = {
container.name: container.is_running(docker, spacetimedb_ping_url)
for container in containers
}
if all(containers_alive.values()):
# sleep a bit more to allow for leader election etc
# TODO: make ping endpoint consider all server state
time.sleep(2)
return
else:
time.sleep(1)

raise Exception("Not all containers are up and running")
raise Exception(f"Not all containers are up and running: {containers_alive!r}")

def spacetimedb_ping_url(port: int) -> str:
return f"http://127.0.0.1:{port}/v1/ping"
Expand Down
41 changes: 33 additions & 8 deletions smoketests/tests/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import xmltodict

import smoketests
from .. import Smoketest, STDB_DIR, run_cmd, TEMPLATE_CARGO_TOML
from .. import Smoketest, STDB_DIR, run_cmd, TEMPLATE_CARGO_TOML, TYPESCRIPT_BINDINGS_PATH, build_typescript_sdk, pnpm


def _write_file(path: Path, content: str):
Expand All @@ -19,12 +19,13 @@ def _append_to_file(path: Path, content: str):
f.write(content)


def _parse_quickstart(doc_path: Path, language: str) -> str:
def _parse_quickstart(doc_path: Path, language: str, module_name: str) -> str:
"""Extract code blocks from `quickstart.md` docs.
This will replicate the steps in the quickstart guide, so if it fails the quickstart guide is broken.
"""
content = Path(doc_path).read_text()
blocks = re.findall(rf"```{language}\n(.*?)\n```", content, re.DOTALL)
codeblock_lang = "ts" if language == "typescript" else language
blocks = re.findall(rf"```{codeblock_lang}\n(.*?)\n```", content, re.DOTALL)

end = ""
if language == "csharp":
Expand All @@ -42,7 +43,7 @@ def _parse_quickstart(doc_path: Path, language: str) -> str:
filtered_blocks.append(block)
blocks = filtered_blocks
# So we could have a different db for each language
return "\n".join(blocks).replace("quickstart-chat", f"quickstart-chat-{language}") + end
return "\n".join(blocks).replace("quickstart-chat", module_name) + end

def load_nuget_config(p: Path):
if p.exists():
Expand Down Expand Up @@ -101,6 +102,8 @@ class BaseQuickstart(Smoketest):
MODULE_CODE = ""

lang = None
client_lang = None
codeblock_langs = None
server_doc = None
client_doc = None
server_file = None
Expand All @@ -118,12 +121,16 @@ def project_init(self, path: Path):
def sdk_setup(self, path: Path):
raise NotImplementedError

@property
def _module_name(self):
return f"quickstart-chat-{self.lang}"

def _publish(self) -> Path:
base_path = Path(self.enterClassContext(tempfile.TemporaryDirectory()))
server_path = base_path / "server"

self.generate_server(server_path)
self.publish_module(f"quickstart-chat-{self.lang}", capture_stderr=True, clear=True)
self.publish_module(self._module_name, capture_stderr=True, clear=True)
return base_path / "client"

def generate_server(self, server_path: Path):
Expand All @@ -141,7 +148,7 @@ def generate_server(self, server_path: Path):
)
self.project_path = server_path / "spacetimedb"
shutil.copy2(STDB_DIR / "rust-toolchain.toml", self.project_path)
_write_file(self.project_path / self.server_file, _parse_quickstart(self.server_doc, self.lang))
_write_file(self.project_path / self.server_file, _parse_quickstart(self.server_doc, self.lang, self._module_name))
self.server_postprocess(self.project_path)
self.spacetime("build", "-d", "-p", self.project_path, capture_stderr=True)

Expand All @@ -163,13 +170,14 @@ def _test_quickstart(self):

run_cmd(*self.build_cmd, cwd=client_path, capture_stderr=True)

client_lang = self.client_lang or self.lang
self.spacetime(
"generate", "--lang", self.lang,
"generate", "--lang", client_lang,
"--out-dir", client_path / self.module_bindings,
"--project-path", self.project_path, capture_stderr=True
)
# Replay the quickstart guide steps
main = _parse_quickstart(self.client_doc, self.lang)
main = _parse_quickstart(self.client_doc, client_lang, self._module_name)
for src, dst in self.replacements.items():
main = main.replace(src, dst)
main += "\n" + self.extra_code
Expand Down Expand Up @@ -322,3 +330,20 @@ def test_quickstart(self):
if not smoketests.HAVE_DOTNET:
self.skipTest("C# SDK requires .NET to be installed.")
self._test_quickstart()

# We use the Rust client for testing the TypeScript server quickstart because
# the TypeScript client quickstart is a React app, which is difficult to
# smoketest.
class TypeScript(Rust):
lang = "typescript"
client_lang = "rust"
server_doc = STDB_DIR / "docs/docs/06-Server Module Languages/05-typescript-quickstart.md"
server_file = "src/index.ts"

def server_postprocess(self, server_path: Path):
build_typescript_sdk()
pnpm("install", TYPESCRIPT_BINDINGS_PATH, cwd=server_path)

def test_quickstart(self):
"""Run the TypeScript quickstart guides for server."""
self._test_quickstart()
Loading