Core (cargo test -p pg_automerge_core): unit tests in the modules, and
in crates/pg_automerge_core/tests/: basics.rs (normalization, merge,
the accumulator), edge_cases.rs (odd documents, scalars, encodings,
corrupted bytes), merge_changes.rs (merge(automerge, bytea) and
automerge_contains(automerge, bytea)), heads_fast_path.rs (the header
parser against Automerge::load().get_heads() over hundreds of generated
documents: empty, up to 12 actors, random forks and merges, merge and
merge_agg outputs, arbitrary prefixes, and the heads and change-count
shortcuts of merge and automerge_contains against the loaded
history, for stored values and saves), history.rs (checked against
Automerge's fork_at / get_changes and a dependency-graph walk, and the
change count against the loaded change graph), loaded.rs (loaded
documents are byte-identical to the flat path), memory_bounds.rs (the
load memory limit: measured peaks against the estimate, see
Resource limits), blocks.rs and
deep_structures.rs (the block check, and every entry point on
documents nested 20,000 levels deep on a 1 MB stack, see
Deep blocks), stats.rs (the live document and load
counters, see Memory observability). common/
holds the
random-history generator and stored-bytes wrappers around the
Input-based API.
#[pg_test]s (cargo pgrx test pg18) for SQL behaviour, in
src/tests/{io,merge,history,notify,expanded,hardening,loads,spans,blocks,memory}.rs
(loads.rs counts the Automerge::load calls of each write path and of
automerge_contains, and
covers the release of the cast's expanded values and the
pg_automerge.verify_writes setting; limits.rs the load memory limit,
see Resource limits; memory.rsautomerge_memory_usage(), see
Memory observability). They are include!d
into the #[pg_schema] mod tests in lib.rs rather than declared as
submodules, because pgrx runs each test as a function of the tests
schema and only items of that exact module go there. cargo pgrx test
installs its pg_test build into pgrx's Postgres, so a server started
afterwards (or a new backend of a running one) runs that build until the
next cargo pgrx install; mise run test installs a plain build after
the pg_tests for the scripts. Test documents are
built in Rust with automerge::AutoCommit and passed in as bytea.
tests/pg_regress (mise run regress): user-facing examples. Its
fixtures are generated by crates/pg_automerge_core/examples/gen_regress.rs,
and mise run regress first checks that they match
(tests/check_regress_fixtures.sh). --resetdb is passed because a
reused regress database would keep the extension objects of an earlier
build.
tests/concurrency.sh (mise run concurrency, also part of
mise run test) runs two real psql sessions against one row to check
the EvalPlanQual claim above, the upsert path and REPEATABLE READ, two
sessions persisting only incremental changes with
merge(doc, $1::bytea) (plus the orphaned-changes rejection), the
WHERE NOT automerge_contains(doc, $1) pattern under EvalPlanQual, and a
pg_dump/restore round trip.
tests/notify.sh (mise run notify, also part of mise run test)
checks automerge_notify() with a real LISTEN session: pg_tests run
inside one transaction that is rolled back, so NOTIFY never delivers
there (the pg_tests instead read what the trigger sent from a test-only
per-backend list).
tests/dump.sh (mise run dump, part of mise run test): plain and
custom-format pg_dump/pg_restore --exit-on-error of a database with
the extension in its own schema (not on the restore's search_path), a
stored generated doc::jsonb column, GIN and btree expression indexes,
an automerge_notify() trigger, a check constraint, views with merge,
|| and merge_agg, and a TOASTed value; checks fingerprints (bytes,
heads, jsonb), that the indexes are valid and used and that the trigger
fires after the restore; binary and text COPY round trips, and a
corrupt value failing COPY FROM with 22P02; a restore into a
database with a lower pg_automerge.max_load_memory failing with
53400, and succeeding with PGOPTIONS='-c pg_automerge.max_load_memory=-1'. A restore and COPY FROM validate every value (one load each; the generated column is
recomputed, one more conversion).
tests/upgrade.sh (mise run upgrade, part of mise run test) and
tests/docker_upgrade.sh (mise run docker-upgrade-test, Docker): see
Upgrade tests.
tests/extension.sh (mise run extension, part of mise run test):
the control-file flags, see
Installation, schema and privileges:
CREATE EXTENSION .. SCHEMA, ALTER EXTENSION .. SET SCHEMA under
dependent objects, a dump and restore of the moved extension, a
non-superuser's refused CREATE EXTENSION, and
pg_automerge.verify_writes and pg_automerge.max_load_memory staying
superuser-only, and misspelled pg_automerge.* settings being removed
when the library loads (the prefix is reserved).
tests/limits.sh (mise run limits, part of mise run test): the load
memory limit against a real crash, in a scratch cluster whose address
space is capped (see Resource limits), and a
document with a block nested 5,000 levels deep read through every path
there without a crash (see Deep blocks). In its
container mode (LIMITS_CONTAINER, used by tests/docker.sh) the
capped server is a container of the Docker image started with
--memory (see Docker image).
tests/memory.sh (mise run memory, part of mise run test):
automerge_memory_usage() across top-level statements, transactions
and errors, see Memory observability.
tests/replication.sh (mise run replication, not part of mise run test: it runs its own scratch cluster with wal_level = logical):
logical replication in text and binary mode (see
Limitations and gotchas).
Fuzzing: crates/pg_automerge_core/tests/fuzz.rs mutates real
Automerge output at the chunk level (bits, bytes, insertions, deletions,
dropped, duplicated and reordered chunks) and recomputes lengths and
checksums, so the mutations reach the decoders. For every input:
nothing panics out of the core, normalize agrees exactly with a plain
load-save-load, its results are stored values (load, header heads,
idempotent), merge(automerge, bytea) results load back with their
heads, the load memory scan never takes input Automerge loads for
unparseable, and normalize's peak (a counting allocator) stays below
the estimate, also when Automerge refuses the input; the block check
(see Deep blocks) never panics on any of the bytes, and
answers for a stored value as for a save of its loaded document. The seeds include
change chunks whose header lists repeat entries. 1,500 inputs per cargo test; mise run fuzz runs 200,000 (or
FUZZ_ITERS) and can save findings (FUZZ_SAVE_DIR) for
tests/corpus/, which runs first. In 300,000 inputs it caught 4,903
decoder panics (all 22P02) and 19 inputs that load but whose re-save
does not ("mismatching heads" on the reload; all of them several
chunks, so never candidates for the compressed-input shortcut), all
rejected by the save-and-load check, and found no property violation.
Two of the latter are in the corpus; the deferred check of merge
results is tested with the test hook (see
The deferred verification).
tests/spans.rs: automerge_spans against Automerge's own spans (see
Rich text spans).
tests/json_walk.rs: the one-sweep jsonb walk, and the choice between
the walks, against the per-object walk; tests/normalize.rs: the compressed-input shortcut against the
full check; tests/interrupts.rs: the interrupt hook runs in the loops
and its errors pass the guard; tests/format_fixtures.rs: values saved
by every shipped automerge version (see
Versioning and upgrades).
tests/bench_expanded.sh (mise run bench-expanded, not part of
mise run test) installs a release build and times the SQL workloads
of Performance on three generated documents (and a
rich text, rich20k, when BENCH_DOCS names it);
tests/bench_sql.sh (mise run bench-sql) times the everyday paths
(reads, inserts, the merge and upsert forms of a write, merge_agg,
automerge_contains) one statement at
a time through psql, writes in BEGIN .. ROLLBACK, median of three
runs; mise run bench-core times the Rust primitives
(examples/bench_core.rs).
CI (.github/workflows/ci.yml) runs mise run ci (lint, test, regress)
on every push and pull request, and the benchmarks nightly (uploaded as a
workflow-run artifact). Runs are grouped by event and ref, so a newer
push cancels an older push run on the same ref but never the nightly
run, and the other way round. For a release (the release workflow
calls it with release: true) it installs the PGDG Postgres 18, runs
PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config mise run package,
unpacks the tarball into / and runs CREATE EXTENSION in that server,
then uploads the tarball, which the release workflow attaches to the
GitHub release (see Releasing).
mise run package (scripts/package.sh) requires PG_CONFIG and
refuses one under $PGRX_HOME (also through a symlink) or of another
major version: cargo pgrx package mirrors that pg_config's install
paths, so a package built for pgrx's development Postgres would unpack
into ~/.pgrx/.... It builds with --locked (a stale Cargo.lock
fails). It then checks that the tarball holds
pg_automerge.so under --pkglibdir and the control file under
--sharedir/extension. tests/check_ci.sh (part of mise run lint)
checks these refusals and the workflow's concurrency group and package
step, the Docker packaging statically (see Docker image),
and the workflows with actionlint, the invariants of ci.yml's jobs
and of the release workflow (only its publish job may push or write,
after every test job), and the release scripts' order and notes (see
Docker image in CI and
Releasing).
mise run docker-test (tests/docker.sh, not part of mise run test)
builds the Docker image and tests it and compose.yaml against
throwaway containers, including the regress examples, five of the
multi-session scripts, limits.sh and a soak smoke run (see Docker image);
CI's docker job runs it on every push and pull request (see Docker
image in CI).
The multi-session shell scripts share tests/lib.sh; they start the pgrx-managed
Postgres if it is not running and stop it again only if they started it.
With PG_AUTOMERGE_TEST_HOST (and _PORT, _USER, _PASSWORD) set
they run against that server instead (external mode): no install, no
start or stop, the connection arguments of every psql, pg_dump and
pg_restore taken from there (the client tools are PG_CONFIG's: pgrx's
pg18 build by default, PGDG's postgresql-client-18 in CI's docker job).
concurrency.sh, notify.sh, dump.sh, extension.sh (its plain role
gets a password, for servers that do not trust TCP connections),
memory.sh and bench_sql.sh work in both modes; upgrade.sh (it copies SQL scripts
into the server's extension directory) and replication.sh (its own
scratch cluster) refuse external mode rather than test something else.
mise run lint: tests/check_ci.sh (with actionlint and shellcheck, pinned in mise.toml), rustfmt, clippy with -D warnings for the default, the
pg_test and the core-only builds, and rustdoc with -D warnings.
tests/soak.sh (mise run soak, not part of mise run test; a
60-second smoke run is part of mise run docker-test): a long
concurrent load against the Docker image, see Soak test.