diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..c047a49 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,20 @@ +--- + +name: Test +on: + pull_request: + push: + branches: + - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + make-helpers: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: php_tests_worktree + run: ./test/php_tests_worktree.sh diff --git a/docker.mk b/docker.mk index f4c572a..e7b3d87 100644 --- a/docker.mk +++ b/docker.mk @@ -5,6 +5,13 @@ NAME ?= default DKR_COMPOSE_FILE ?= -f $(PWD)/docker-compose.yaml DKR_COMPOSE_PROJECT = $(NAME) +# path to the source tree bind-mounted into the containers; defaults to the main +# checkout. override to run against a git worktree instead (see php_tests_worktree +# in php.mk). exported so `docker compose` can interpolate ${DKR_COMPOSE_SRC} in +# your compose file's volume definitions. +DKR_COMPOSE_SRC ?= $(PWD) +export DKR_COMPOSE_SRC + ifneq ($(shell docker compose --version),) DKR_COMPOSE_CMD = COMPOSE_PROFILES=$(DKR_COMPOSE_PROFILES) docker compose $(DKR_COMPOSE_FILE) -p $(DKR_COMPOSE_PROJECT) else diff --git a/docs/docker.md b/docs/docker.md index 9c54a2e..aaafd45 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -3,6 +3,7 @@ * [setup](#setup) * [variables](#variables) * [docker compose file](#docker-compose-file) + * [source path](#source-path) * [commands](#commands) * [dkr_pull](#dkr_pull) * [dkr_build](#dkr_build) @@ -29,6 +30,25 @@ The variable `$(GIT_TAG)` comes from [common.mk](../common.mk) and documented in dkr_build: DKR_COMPOSE_FILE = -f ./docker-compose.build.yaml ``` +### source path + +`DKR_COMPOSE_SRC` is the path to the source tree bind-mounted into the containers. +It defaults to `$(PWD)` (the main checkout) and is exported so `docker compose` can +interpolate it inside your compose file: + +```yaml +services: + cli: + # no `container_name:` — lets an isolated run coexist with the main stack + volumes: + - ${DKR_COMPOSE_SRC:-.}:/app +``` + +Referencing `${DKR_COMPOSE_SRC}` instead of a hardcoded path, and omitting +`container_name`, is what lets [`php_tests_worktree`](./php.md#tests-against-a-worktree) +run a git worktree's code against the already-running main-checkout services without +conflicting with them. + ## commands ### dkr_pull diff --git a/docs/php.md b/docs/php.md index f78676d..2914ff1 100644 --- a/docs/php.md +++ b/docs/php.md @@ -8,6 +8,7 @@ * [composer install](#composer-install) * [composer update](#composer-update) * [tests](#tests) + * [tests against a worktree](#tests-against-a-worktree) ## setup @@ -49,3 +50,37 @@ make php_tests_phpunit ```shell make php_tests_behat ``` + +### tests against a worktree + +Run the suite against a git worktree's code while reusing the already-running local +services (MySQL / Redis / Elasticsearch), with per-worktree database isolation so +parallel worktree runs don't collide with each other or the main checkout: + +```shell +make php_tests_worktree WORKTREE=/path/to/worktree +``` + +Pick a suite, or run an arbitrary command, with `TESTS` / `COMMAND`: + +```shell +make php_tests_worktree WORKTREE=/path/to/worktree TESTS=behat +make php_tests_worktree WORKTREE=/path/to/worktree COMMAND="php artisan test" +``` + +How it works: + +* **source** — the worktree is bind-mounted via `DKR_COMPOSE_SRC` (see + [docker.md](./docker.md#source-path)) rather than the main checkout. +* **services** — the container runs with `--no-deps`, reusing the services you already + have `up` instead of starting a duplicate stack, so bring the main stack up first. +* **database isolation** — the run is given a unique, sanitised `DB_DATABASE` derived from + the worktree path (override with `DB_DATABASE=...`). The shared MySQL server is reused; + only the schema differs. Ensure your test bootstrap creates and migrates it (e.g. + Laravel's `RefreshDatabase`). +* **composer drift** — if the worktree's `composer.lock` differs from the main + checkout's (or its `vendor/` is missing) dependencies are installed for the worktree + first, otherwise the existing `vendor/` is reused. + +This requires your compose file to reference `${DKR_COMPOSE_SRC}` for the code volume and +to avoid a hardcoded `container_name` — see [docker.md](./docker.md#source-path). diff --git a/php.mk b/php.mk index 2fd2528..a3d6e99 100644 --- a/php.mk +++ b/php.mk @@ -14,6 +14,32 @@ php_composer_update: php_tests_%: $(DKR_COMPOSE_CMD_UP) || { $(DKR_COMPOSE_CMD_DOWN); exit 1; } +# php_tests_worktree — run the test suite against a git worktree's code while +# reusing the already-running local services (MySQL / Redis / Elasticsearch), +# with per-worktree database isolation so parallel worktree runs don't collide +# with each other or the main checkout. bring the main stack up first. +# +# make php_tests_worktree WORKTREE=/path/to/worktree +# make php_tests_worktree WORKTREE=/path/to/worktree TESTS=behat +# make php_tests_worktree WORKTREE=/path/to/worktree COMMAND="php artisan test" +php_tests_worktree: CONTAINER ?= cli +php_tests_worktree: WORKTREE ?= $(PWD) +php_tests_worktree: WORKTREE_SRC = $(abspath $(WORKTREE)) +php_tests_worktree: WORKTREE_ID = $(notdir $(WORKTREE_SRC)) +php_tests_worktree: DKR_COMPOSE_SRC = $(WORKTREE_SRC) +# db name: sanitised worktree basename plus a hash of the full path, so two +# worktrees that share a basename under different parents don't collide and odd +# characters (dots, slashes) can't produce an invalid identifier. +php_tests_worktree: DB_DATABASE ?= $(shell printf 'test_%s_%s' "$$(printf '%s' '$(WORKTREE_ID)' | tr -c 'A-Za-z0-9' '_')" "$$(printf '%s' '$(WORKTREE_SRC)' | cksum | cut -d' ' -f1)") +php_tests_worktree: TESTS ?= phpunit +php_tests_worktree: COMMAND ?= vendor/bin/$(TESTS) +php_tests_worktree: DKR_COMPOSE_ADDITIONAL_RUN = --no-deps --env DB_DATABASE=$(DB_DATABASE) +php_tests_worktree: + @test -n "$(WORKTREE)" && test -d "$(WORKTREE_SRC)" || { echo "php_tests_worktree: WORKTREE '$(WORKTREE)' does not exist" >&2; exit 1; } + @echo "php_tests_worktree: running '$(COMMAND)' against $(WORKTREE_SRC) (db: $(DB_DATABASE))" + @if [ ! -d "$(WORKTREE_SRC)/vendor" ] || ! cmp -s "$(WORKTREE_SRC)/composer.lock" "$(PWD)/composer.lock"; then echo "php_tests_worktree: composer.lock drift detected - installing dependencies for '$(WORKTREE_ID)'"; $(DKR_COMPOSE_CMD) run --rm $(DKR_COMPOSE_ADDITIONAL)--no-deps $(CONTAINER) composer install; else echo "php_tests_worktree: composer.lock matches main checkout - reusing vendor"; fi + $(DKR_COMPOSE_CMD_RUN) + php_cmd_%: CONTAINER ?= cli php_cmd_%: $(DKR_COMPOSE_CMD_RUN) || { $(DKR_COMPOSE_CMD_DOWN); exit 1; } diff --git a/test/php_tests_worktree.sh b/test/php_tests_worktree.sh new file mode 100755 index 0000000..e26ec7b --- /dev/null +++ b/test/php_tests_worktree.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# +# Verifies the `php_tests_worktree` make target by inspecting the commands it +# would run (`make -n`), so it needs only `make` — no Docker. Checks that the +# flow reuses the already-running services, isolates the database per worktree, +# runs against the worktree's source, and handles composer/vendor drift. + +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +worktree=/tmp/wt-fixture-test + +if ! rendered=$(make -n \ + -f "$repo_root/docker.mk" \ + -f "$repo_root/php.mk" \ + php_tests_worktree WORKTREE="$worktree" 2>&1); then + echo "FAIL: 'make -n php_tests_worktree' errored:" >&2 + printf '%s\n' "$rendered" >&2 + exit 1 +fi + +echo "rendered commands:" +printf '%s\n' "$rendered" | sed 's/^/ | /' +echo + +failures=0 + +expect() { # description, fixed-string + if printf '%s\n' "$rendered" | grep -qF -- "$2"; then + echo "ok - $1" + else + echo "FAIL - $1 (expected to find: $2)" + failures=$((failures + 1)) + fi +} + +refute() { # description, fixed-string + if printf '%s\n' "$rendered" | grep -qF -- "$2"; then + echo "FAIL - $1 (did not expect: $2)" + failures=$((failures + 1)) + else + echo "ok - $1" + fi +} + +# AC1 — runs against the specified worktree's code, and runs the suite +expect "mounts the worktree source" "$worktree" +expect "runs the selected test suite" "vendor/bin/phpunit" +# AC2 — per-worktree database isolation (sanitised basename + hash of full path) +expect "isolates the database per worktree" "DB_DATABASE=test_wt_fixture_test_3943437479" +# AC3 — reuses the already-running services, no duplicate full stack +expect "reuses running services (--no-deps)" "--no-deps" +refute "does not start a full stack (up)" "up --abort-on-container-exit" +# AC4 — composer/vendor drift handled +expect "compares composer.lock for drift" "composer.lock" +expect "installs dependencies on drift" "composer install" + +echo +if [ "$failures" -ne 0 ]; then + echo "php_tests_worktree: $failures assertion(s) FAILED" + exit 1 +fi +echo "php_tests_worktree: all assertions passed"