From 3f675e3ea77d0a2567d06b17f53a1cf8efb50837 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Tue, 9 Jun 2026 23:15:51 +0200 Subject: [PATCH] add turnkey end-to-end testing against real CouchDB - support/run-e2e.sh: run the integration suite group-by-group (isolation) against a CouchDB given by COUCHDB_URL/USER/PASS - docker-compose.yml + Makefile: `make e2e` starts CouchDB, runs the suite, tears it down; `make e2e-up`/`e2e-down` manage it standalone - CI integration job now runs all 15 suite groups via the shared script (was 11; view_ops, design_ops, changes_ops, error_handling were skipped) - fix the four previously-skipped groups, which had test bugs: - all_docs/view_first: use the include_docs atom option, not a tuple - view_cleanup: view_cleanup/1 returns ok, not {ok, Map} - view_count: count/1 returns a bare integer - view_fold: correct fold/5 argument order and accumulator contract - error_invalid_doc: open_db/2 is local; assert on db_info instead - README: document unit and e2e test commands --- .github/workflows/test.yml | 10 +----- Makefile | 54 ++++++++++++++++++++++++++++ README.md | 25 +++++++++++++ docker-compose.yml | 15 ++++++++ support/run-e2e.sh | 29 +++++++++++++++ test/couchbeam_integration_SUITE.erl | 41 ++++++++++----------- 6 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100755 support/run-e2e.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 953ba3d3..23b66acc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -181,12 +181,4 @@ jobs: COUCHDB_URL: http://localhost:5984 COUCHDB_USER: admin COUCHDB_PASS: admin - run: | - # Run each test group individually for better isolation - for group in server_ops database_ops document_ops bulk_ops attachment_ops \ - view_streaming_ops changes_streaming_ops replication_ops \ - db_management_ops mango_ops uuid_ops; do - echo "=== Running $group ===" - rebar3 ct --suite=couchbeam_integration_SUITE --group=$group --readable=true || exit 1 - done - echo "All integration tests passed" + run: ./support/run-e2e.sh diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d17130c1 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# couchbeam developer tasks. + +COUCHDB_URL ?= http://127.0.0.1:5984 +COUCHDB_USER ?= admin +COUCHDB_PASS ?= admin +COMPOSE ?= docker compose + +export COUCHDB_URL COUCHDB_USER COUCHDB_PASS + +.PHONY: all compile eunit xref dialyzer test e2e e2e-up e2e-run e2e-down clean + +all: compile + +compile: + rebar3 compile + +eunit: + rebar3 eunit + +xref: + rebar3 xref + +dialyzer: + rebar3 dialyzer + +test: eunit xref + +## Run the end-to-end suite against a throwaway CouchDB (start, run, stop). +e2e: e2e-up + @./support/run-e2e.sh; status=$$?; $(COMPOSE) down -v; exit $$status + +## Start CouchDB and create the system databases. +e2e-up: + $(COMPOSE) up -d + @echo "Waiting for CouchDB at $(COUCHDB_URL)..." + @for i in $$(seq 1 60); do \ + curl -sf $(COUCHDB_URL)/_up >/dev/null 2>&1 && break; \ + sleep 1; \ + done + @for db in _users _replicator _global_changes; do \ + curl -s -u $(COUCHDB_USER):$(COUCHDB_PASS) -X PUT $(COUCHDB_URL)/$$db >/dev/null || true; \ + done + @echo "CouchDB ready at $(COUCHDB_URL)" + +## Run the e2e suite against an already-running CouchDB. +e2e-run: + ./support/run-e2e.sh + +## Stop and remove the CouchDB container and its data. +e2e-down: + $(COMPOSE) down -v + +clean: + rebar3 clean diff --git a/README.md b/README.md index 887da895..d555b4a1 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,31 @@ ok = couchbeam:delete_attachment(Db, Doc, "file.txt"). | `couchbeam_changes` | Changes feed | | `couchbeam_attachments` | Inline attachment helpers | +## Testing + +Unit tests run without any external services: + +``` +rebar3 eunit +``` + +End-to-end tests run the full client against a real CouchDB. With Docker +available, one command starts CouchDB, runs the suite, and tears it down: + +``` +make e2e +``` + +To use a CouchDB you already run, point the runner at it: + +``` +COUCHDB_URL=http://127.0.0.1:5984 COUCHDB_USER=admin COUCHDB_PASS=admin \ + ./support/run-e2e.sh +``` + +`make e2e-up` / `make e2e-down` start and stop the bundled CouchDB +(`docker-compose.yml`) on their own. + ## Contributing Found a bug or have a feature request? [Open an issue](https://github.com/benoitc/couchbeam/issues). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..4de77a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +# Local CouchDB used by the couchbeam end-to-end tests. +# See `make e2e` (or support/run-e2e.sh) for the test runner. +services: + couchdb: + image: couchdb:3.3 + environment: + COUCHDB_USER: admin + COUCHDB_PASSWORD: admin + ports: + - "5984:5984" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5984/_up"] + interval: 5s + timeout: 5s + retries: 20 diff --git a/support/run-e2e.sh b/support/run-e2e.sh new file mode 100755 index 00000000..426ae6ca --- /dev/null +++ b/support/run-e2e.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +# +# Run the couchbeam end-to-end suite against a real CouchDB. +# +# Each test group runs as a separate `rebar3 ct` invocation so every group +# starts from a fresh suite setup (its own database), which keeps them +# isolated. Honors: +# COUCHDB_URL (default http://127.0.0.1:5984) +# COUCHDB_USER (default empty, i.e. no auth) +# COUCHDB_PASS (default empty) +# +set -eu + +COUCHDB_URL="${COUCHDB_URL:-http://127.0.0.1:5984}" +COUCHDB_USER="${COUCHDB_USER:-}" +COUCHDB_PASS="${COUCHDB_PASS:-}" +export COUCHDB_URL COUCHDB_USER COUCHDB_PASS + +e2e_groups="server_ops database_ops document_ops bulk_ops attachment_ops \ + view_ops design_ops changes_ops error_handling \ + view_streaming_ops changes_streaming_ops replication_ops \ + db_management_ops mango_ops uuid_ops" + +for group in $e2e_groups; do + echo "=== e2e group: ${group} ===" + rebar3 ct --suite=couchbeam_integration_SUITE --group="${group}" --readable=true +done + +echo "All e2e groups passed" diff --git a/test/couchbeam_integration_SUITE.erl b/test/couchbeam_integration_SUITE.erl index d821de78..f110e4cd 100644 --- a/test/couchbeam_integration_SUITE.erl +++ b/test/couchbeam_integration_SUITE.erl @@ -822,7 +822,7 @@ all_docs(Config) -> {ok, _} = couchbeam:save_docs(Db, Docs), %% Query all_docs - {ok, AllDocs} = couchbeam_view:all(Db, [{include_docs, true}]), + {ok, AllDocs} = couchbeam_view:all(Db, [include_docs]), true = length(AllDocs) >= 10, %% Verify structure @@ -985,9 +985,9 @@ view_reduce(Config) -> view_count(Config) -> Db = ?config(db, Config), - %% Count all documents - {ok, Count} = couchbeam_view:count(Db), - true = Count > 0, + %% Count all documents (count/1 returns a bare integer) + Count = couchbeam_view:count(Db), + true = is_integer(Count) andalso Count > 0, ct:pal("Document count: ~p", [Count]), ok. @@ -997,7 +997,7 @@ view_first(Config) -> Db = ?config(db, Config), %% Get first document - {ok, First} = couchbeam_view:first(Db, [{include_docs, true}]), + {ok, First} = couchbeam_view:first(Db, 'all_docs', [include_docs]), true = is_map(First), true = maps:is_key(<<"id">>, First), ct:pal("First document: ~p", [maps:get(<<"id">>, First)]), @@ -1008,19 +1008,21 @@ view_first(Config) -> view_fold(Config) -> Db = ?config(db, Config), - %% Fold over documents counting them - {ok, FoldCount} = couchbeam_view:fold(Db, fun(_Row, Acc) -> - {ok, Acc + 1} - end, 0, []), + %% Fold over documents counting them. fold/5 is + %% fold(Function, Acc, Db, ViewName, Options); the function returns the + %% next accumulator (or stop) and fold returns the final accumulator. + FoldCount = couchbeam_view:fold(fun(_Row, Acc) -> + Acc + 1 + end, 0, Db, 'all_docs', []), - true = FoldCount > 0, + true = is_integer(FoldCount) andalso FoldCount > 0, ct:pal("Fold counted ~p documents", [FoldCount]), %% Fold collecting IDs - {ok, Ids} = couchbeam_view:fold(Db, fun(Row, Acc) -> + Ids = couchbeam_view:fold(fun(Row, Acc) -> Id = maps:get(<<"id">>, Row), - {ok, [Id | Acc]} - end, [], [{limit, 5}]), + [Id | Acc] + end, [], Db, 'all_docs', [{limit, 5}]), 5 = length(Ids), ok. @@ -1057,11 +1059,10 @@ design_info(Config) -> view_cleanup(Config) -> Db = ?config(db, Config), - %% Trigger view cleanup - {ok, Result} = couchbeam:view_cleanup(Db), - true = maps:get(<<"ok">>, Result, false), + %% Trigger view cleanup (returns ok) + ok = couchbeam:view_cleanup(Db), - ct:pal("View cleanup result: ~p", [Result]), + ct:pal("View cleanup triggered"), ok. %%==================================================================== @@ -1202,11 +1203,11 @@ error_conflict(Config) -> error_invalid_doc(Config) -> Server = ?config(server, Config), - %% Try to open non-existent database - {error, not_found} = couchbeam:open_db(Server, <<"this_db_does_not_exist_xyz">>), + %% open_db is a local operation; a missing database only surfaces on use + {ok, MissingDb} = couchbeam:open_db(Server, <<"this_db_does_not_exist_xyz">>), + {error, db_not_found} = couchbeam:db_info(MissingDb), %% Try to create database with invalid name (empty) - %% Note: CouchDB may accept some unusual names, so we test with clearly invalid chars {error, _} = couchbeam:create_db(Server, <<"">>), ct:pal("Invalid document/database errors handled correctly"),