pg_automergev0.1.0

Testing

  • 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.rs automerge_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.