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
40 changes: 23 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,26 @@ jobs:
rustup default stable
- uses: taiki-e/install-action@v2.77.4
with:
tool: cargo-nextest
- run: |
sudo systemctl start postgresql.service
sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'"
sudo -u postgres createdb --owner=canopy canopy
tool: cargo-nextest,just
- uses: Swatinem/rust-cache@v2
- run: cargo nextest run
env:
DATABASE_URL: postgresql://canopy:canopy@localhost:5432/canopy
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
cache-dependency-path: private-web/package-lock.json
# The private-server tests serve the embedded React SPA via rust-embed,
# so they need private-web/dist/ to exist. `just test` sets
# SKIP_FRONTEND_BUILD=1 (build.rs won't build it) and a fresh checkout has
# no dist/, so build it explicitly here. Dev machines already have a dist/
# from working on the frontend.
- name: Build frontend
run: npm ci && npm run build
working-directory: private-web
# `just test` runs nextest against a throwaway tmpfs Postgres that
# scripts/ramdisk-pg.sh spins up with the runner's own initdb/pg_ctl
# (preinstalled on ubuntu-latest), so no system Postgres service or
# role/db setup is needed here — same path developers run locally.
- run: just test

clippy:
name: Clippy
Expand Down Expand Up @@ -77,12 +88,6 @@ jobs:
node-version: lts/*
cache: npm
cache-dependency-path: private-web/package-lock.json
- name: Start Postgres
# Mirrors the canopy/canopy role + admin DB used by the rust test job.
run: |
sudo systemctl start postgresql.service
sudo -u postgres psql --command="CREATE ROLE canopy SUPERUSER LOGIN PASSWORD 'canopy'"
sudo -u postgres createdb --owner=canopy canopy
- name: Build private-server + migrate
# SKIP_FRONTEND_BUILD avoids private-server/build.rs running an npm
# install + vite build to embed dist/ — the e2e fixture uses Vite
Expand All @@ -104,10 +109,11 @@ jobs:
run: npx playwright install --with-deps chromium
working-directory: private-web
- name: Playwright tests
run: npm run test:e2e
# The wrapper spins up a throwaway tmpfs Postgres and exports
# CANOPY_E2E_ADMIN_DATABASE_URL pointing at it; the fixture creates its
# per-worker databases on that cluster. Same DB harness as `just test`.
run: ../scripts/ramdisk-pg.sh npm run test:e2e
working-directory: private-web
env:
CANOPY_E2E_ADMIN_DATABASE_URL: postgresql://canopy:canopy@localhost:5432/postgres
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
Expand Down
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@ End-to-end tests use Playwright. Run with `npm run test:e2e` from `/private-web/
- Run specific tests: `just test-name <test_name>`
- Verify no compilation warnings in tests and main code

### Tests run on a throwaway RAM-backed Postgres by default
Each test creates and drops its own database (and runs every migration), and
nextest runs many in parallel. Against a disk-backed Postgres the resulting
`CREATE DATABASE`/`DROP DATABASE` fsync storm saturates disk I/O and can make
the whole machine unresponsive.

So `just test` (and `test-package`/`test-name`/`test-verbose`/`test-e2e`) run
through `scripts/ramdisk-pg.sh`, which spins up a disposable Postgres on tmpfs
(`/dev/shm`) with `fsync`/`synchronous_commit`/`full_page_writes` off, points
`DATABASE_URL` at it, then tears it down. Nothing touches a physical disk, so
there's no grind and runs are dramatically faster. It reuses your installed
`initdb`/`pg_ctl`, so the server version matches your system Postgres with no
container or image to manage. `just test` takes nextest args, so `just test`,
`just test -p database`, and `just test <name>` all work. Wrap other commands
with `just fast <cmd>`.

Requirements/caveats: needs the Postgres *server* tools (`initdb`/`pg_ctl`), not
just the `psql` client. On macOS there's no `/dev/shm`, so it falls back to disk
— still fast (fsync is off), just not RAM-backed unless you point
`CANOPY_TEST_PG_DIR` at a real ramdisk. Other overrides: `CANOPY_TEST_PG_PORT`,
`CANOPY_TEST_PG_ROLE`.

To run against your **system** Postgres instead (to inspect the DB afterwards,
or where `initdb` isn't available), use `just test-system [nextest args]` —
prefix with `nice` to soften the I/O grind.

## Version Control
- If the working copy is a jujutsu repo (a `.jj` directory exists at the repo root), prefer `jj` commands over `git` for VCS operations (status, diff, log, commit/describe, etc.). The repo may be colocated with git, but `jj` is the source of truth for local work.
- If there is no `.jj` directory, use `git` as normal.
Expand Down
43 changes: 31 additions & 12 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,47 @@ watch-private-api:
watch-private-web:
cd private-web && npm run dev

# Run all tests
test:
DATABASE_URL={{ DATABASE_URL }} cargo nextest run

# Run tests for a specific package
# Run all tests. Uses a throwaway RAM-backed Postgres (tmpfs + fsync off) via
# scripts/ramdisk-pg.sh so the per-test CREATE/DROP DATABASE churn never hits
# disk — fast, no I/O grind. Args pass straight to nextest, so `just test`,
# `just test -p database`, and `just test some_name` all work. Use test-system
# to run against $DATABASE_URL instead.
test *args:
scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast {{ args }}

# Run tests for a specific package (RAM-backed; see `test`)
test-package package:
DATABASE_URL={{ DATABASE_URL }} cargo nextest run -p {{ package }}
scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast -p {{ package }}

# Run a specific test
# Run a specific test (RAM-backed; see `test`)
test-name name:
DATABASE_URL={{ DATABASE_URL }} cargo nextest run {{ name }}
scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast {{ name }}

# Run tests with no capture (show output)
# Run tests with no capture (show output) (RAM-backed; see `test`)
test-verbose:
DATABASE_URL={{ DATABASE_URL }} cargo nextest run --no-capture
scripts/ramdisk-pg.sh cargo nextest run --no-fail-fast --no-capture

# Run tests against your system Postgres ($DATABASE_URL) rather than the
# throwaway RAM-backed one — e.g. to inspect the DB afterwards, or where
# initdb/pg_ctl aren't available. Args pass through to nextest. Prefix with
# `nice` to soften the I/O grind. `just test-system`, `just test-system -p
# database`, `just test-system some_name`.
test-system *args:
DATABASE_URL={{ DATABASE_URL }} cargo nextest run --no-fail-fast {{ args }}

# Run any command against the throwaway RAM-backed Postgres (escape hatch for
# things the test recipes don't cover).
fast +cmd:
scripts/ramdisk-pg.sh {{ cmd }}

# Run the private-web Playwright end-to-end suite. Builds the
# private-server + migrate binaries first (the e2e fixture spawns its
# own server/Vite per worker — no `just watch-*` needed).
# own server/Vite per worker — no `just watch-*` needed). Runs against the
# throwaway RAM-backed Postgres; the fixture creates its per-worker databases
# on the cluster the wrapper points CANOPY_E2E_ADMIN_DATABASE_URL at.
test-e2e:
cargo build --bin private-server --bin migrate
cd private-web && npm run test:e2e
cd private-web && {{ justfile_directory() }}/scripts/ramdisk-pg.sh npm run test:e2e

# Same as `test-e2e` but launches Playwright's interactive UI runner.
# Useful for stepping through failures and inspecting traces.
Expand Down
129 changes: 129 additions & 0 deletions scripts/ramdisk-pg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# Run a command against a throwaway, RAM-backed PostgreSQL instance.
#
# Why: the test suite creates and drops a fresh database (plus runs every
# migration) for *each* of its hundreds of tests, dozens in parallel. Against a
# disk-backed cluster the CREATE DATABASE / DROP DATABASE fsync storm saturates
# the machine's I/O and makes the whole system unresponsive. This spins up a
# disposable cluster on tmpfs with durability turned off, so none of that churn
# ever reaches a physical disk.
#
# This is what `just test` uses by default. It's self-contained: it uses the
# `initdb`/`pg_ctl` already on your machine, so the server version matches your
# system Postgres exactly and there's no container runtime or image to manage.
# To run against your system Postgres instead, use `just test-system`.
#
# Usage:
# scripts/ramdisk-pg.sh cargo nextest run -p database
# scripts/ramdisk-pg.sh just test
#
# Env overrides:
# CANOPY_TEST_PG_DIR data directory (default: a fresh dir under /dev/shm)
# CANOPY_TEST_PG_PORT starting TCP port to probe (default: 5433)
# CANOPY_TEST_PG_ROLE superuser role / db owner (default: canopy)
set -euo pipefail

if [ "$#" -eq 0 ]; then
echo "usage: $0 <command> [args...]" >&2
exit 64
fi

ROLE="${CANOPY_TEST_PG_ROLE:-canopy}"

# Locate the Postgres server binaries. They're on PATH on Arch, but other
# installs hide them: Debian/Ubuntu under a versioned dir, Homebrew under
# opt/postgresql@NN, and Postgres.app inside its bundle. Fall back to those.
if ! command -v initdb >/dev/null 2>&1; then
for d in \
/usr/lib/postgresql/*/bin \
/opt/homebrew/opt/postgresql*/bin \
/usr/local/opt/postgresql*/bin \
/Applications/Postgres.app/Contents/Versions/*/bin; do
if [ -x "$d/initdb" ]; then
PATH="$d:$PATH"
break
fi
done
fi
for bin in initdb pg_ctl createdb; do
command -v "$bin" >/dev/null 2>&1 || {
echo "error: '$bin' not found on PATH. Install the Postgres server tools." >&2
exit 69
}
done

# Pick a tmpfs-backed data directory. /dev/shm is tmpfs on Linux; if it's
# missing (e.g. macOS) fall back to $TMPDIR but warn that it won't be RAM-backed.
if [ -n "${CANOPY_TEST_PG_DIR:-}" ]; then
DATADIR="$CANOPY_TEST_PG_DIR"
mkdir -p "$DATADIR"
OWN_DATADIR=0
else
if [ -d /dev/shm ] && [ -w /dev/shm ]; then
BASE=/dev/shm
else
# No tmpfs by default on macOS. We still turn fsync off below, which is
# what actually removes the I/O grind, so this stays fast — it just lands
# on disk. For a true ramdisk, create one and pass CANOPY_TEST_PG_DIR,
# e.g. macOS: diskutil erasevolume APFS canopy-pg $(hdiutil attach -nomount ram://1048576)
BASE="${TMPDIR:-/tmp}"
echo "note: no /dev/shm; using $BASE (disk-backed, but fsync is off so still fast)" >&2
fi
DATADIR="$(mktemp -d "$BASE/canopy-test-pg.XXXXXX")"
OWN_DATADIR=1
fi

# Find a free TCP port, starting from the requested one.
port_in_use() { (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; }
PORT="${CANOPY_TEST_PG_PORT:-5433}"
for _ in $(seq 0 20); do
port_in_use "$PORT" || break
PORT=$((PORT + 1))
done
if port_in_use "$PORT"; then
echo "error: no free port found near ${CANOPY_TEST_PG_PORT:-5433}" >&2
exit 69
fi

STARTED=0
cleanup() {
status=$?
if [ "$STARTED" = 1 ]; then
pg_ctl -D "$DATADIR" -m immediate stop >/dev/null 2>&1 || true
fi
if [ "${OWN_DATADIR:-0}" = 1 ]; then
rm -rf "$DATADIR"
fi
exit "$status"
}
trap cleanup EXIT INT TERM

echo "ramdisk-pg: initialising disposable cluster in $DATADIR (port $PORT)" >&2
# --no-sync: don't fsync the freshly-created cluster; it's disposable.
initdb -D "$DATADIR" -U "$ROLE" --auth=trust --no-sync -E UTF8 >/dev/null

# Durability-off settings are safe here precisely because the data is thrown
# away. max_connections is bumped well past Postgres's default of 100 to absorb
# the parallel test pools. unix_socket_directories points at the data dir so a
# stray socket never lands in a shared /tmp or /run.
pg_ctl -D "$DATADIR" -l "$DATADIR/postmaster.log" -w start -o "\
-p $PORT \
-h 127.0.0.1 \
-k $DATADIR \
-c fsync=off \
-c synchronous_commit=off \
-c full_page_writes=off \
-c autovacuum=off \
-c max_connections=300" >/dev/null
STARTED=1

createdb -h 127.0.0.1 -p "$PORT" -U "$ROLE" "$ROLE"

export DATABASE_URL="postgresql://${ROLE}@127.0.0.1:${PORT}/${ROLE}"
export RO_DATABASE_URL="$DATABASE_URL"
# So `just test-e2e` (whose fixture creates its own per-worker databases) can
# ride on the same RAM-backed cluster when invoked through this wrapper.
export CANOPY_E2E_ADMIN_DATABASE_URL="postgresql://${ROLE}@127.0.0.1:${PORT}/postgres"

echo "ramdisk-pg: DATABASE_URL=$DATABASE_URL" >&2
"$@"