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
20 changes: 20 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---

name: Test
on:
pull_request:
push:
branches:
- main
Comment thread
coderabbitai[bot] marked this conversation as resolved.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
make-helpers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- name: php_tests_worktree
run: ./test/php_tests_worktree.sh
7 changes: 7 additions & 0 deletions docker.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions docs/php.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* [composer install](#composer-install)
* [composer update](#composer-update)
* [tests](#tests)
* [tests against a worktree](#tests-against-a-worktree)

## setup

Expand Down Expand Up @@ -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).
26 changes: 26 additions & 0 deletions php.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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; }
63 changes: 63 additions & 0 deletions test/php_tests_worktree.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading