From 092b1beba1a2da126b86aa4bbdb3c1717f95a33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Wed, 12 Aug 2026 10:37:34 +0200 Subject: [PATCH 1/5] feat(just): split the justfile into just/*.just imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Jochum --- docs | 2 +- just/build.just | 84 +++++++++++ just/incus.just | 153 ++++++++++++++++++++ just/mod.just | 72 ++++++++++ just/test.just | 130 +++++++++++++++++ justfile | 368 ++---------------------------------------------- 6 files changed, 448 insertions(+), 361 deletions(-) create mode 100644 just/build.just create mode 100644 just/incus.just create mode 100644 just/mod.just create mode 100644 just/test.just diff --git a/docs b/docs index d4e9bbd..74cad0b 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit d4e9bbdcdc691879dcbff3b0e48d0939edfbf5da +Subproject commit 74cad0b1301d971ff8bb469232c6dfa5999dddfc diff --git a/just/build.just b/just/build.just new file mode 100644 index 0000000..bc0fe02 --- /dev/null +++ b/just/build.just @@ -0,0 +1,84 @@ +# Building and running incus-compose and its ic-healthd sidecar. + +# Build a dev binary +build: update-healthd + #!/usr/bin/env bash + set -euo pipefail + + image=$(source .env; echo "${INCUS_COMPOSE_HEALTHD_IMAGE:-}") + version="${image##*:}" + + if [[ "${version}" == "${image}" ]]; then + version="`git describe --tags --always --long --dirty="-dirty"`" + fi + + go build -ldflags="-X github.com/lxc/incus-compose/cmd/incus-compose/version.Version=v${version#v}" -o bin/incus-compose ./cmd/incus-compose + +# Usage: just run -f test/fixtures/simple/compose.yaml config +run *args: + @go run ./cmd/incus-compose {{ args }} + +# Build ic-healthd binary +build-healthd: + CGO_ENABLED=0 go build -tags=netgo -ldflags="-w -s -X github.com/lxc/incus-compose/cmd/ic-healthd/version.Version=`git describe --tags --always --long --dirty="-dirty"`" -trimpath -o bin/ic-healthd ./cmd/ic-healthd + +# Build ic-healthd container image +build-healthd-image tag_base="ghcr.io/lxc/incus-compose/ic-healthd": + #!/usr/bin/env bash + set -euo pipefail + + if [[ ! -f .env ]]; then + cp .env.sample .env + fi + + # The random suffix gives a clear sign that your version is running. + VERSION="`git describe --tags --always --long --dirty="-dirty"`-`openssl rand -hex 4`" + # Container tags carry no v prefix, `release` pushes them without one too. + export VERSION="${VERSION#v}" + echo ${VERSION} + + echo "Building for the 'default' cache on '${INCUS_REMOTE}'" + just run -P cmd/ic-healthd build --os-env # os-env cause of VERSION + + if [[ ${INCUS_COMPOSE_IMAGE_CACHE:-} != "incus-compose-tests-cache" ]]; then + echo "Building for the 'incus-compose-tests-cache' cache on '${INCUS_REMOTE}'" + just run --image-cache="incus-compose-tests-cache" -P cmd/ic-healthd build --os-env + fi + + sed -i -e 's|export INCUS_COMPOSE_HEALTHD_IMAGE=".*"|export INCUS_COMPOSE_HEALTHD_IMAGE="{{ tag_base }}:'${VERSION}'"|g' .env + +# Rebuild the ic-healthd image and put the shared daemon on it +update-healthd *args="--trace": build-healthd-image + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + + echo "Deleting the global ic-healthd on remote '${remote}'" + echo "yes" | incus project rm --force "incus-compose" || true + + # New image + export INCUS_COMPOSE_HEALTHD_IMAGE=$(source .env; echo "$INCUS_COMPOSE_HEALTHD_IMAGE") + echo "Image ${INCUS_COMPOSE_HEALTHD_IMAGE}" + + # The healthd-scope project is left behind as the handle for `healthd logs`. + just run healthd up {{ args }} + +# Build ic-healthd container image +release-healthd-image tag="ghcr.io/lxc/incus-compose/ic-healthd:latest": + #!/usr/bin/env bash + set -euo pipefail + + VERSION="`git describe --tags --always --long --dirty="-dirty"`-`openssl rand -hex 4`" + + # New image + podman build --tag "{{ tag }}" --build-arg VERSION="${VERSION}" -f ./cmd/ic-healthd/Dockerfile . + echo "Image ${INCUS_COMPOSE_HEALTHD_IMAGE}" + + echo "${GITHUB_TOKEN}" | podman login --username "${GITHUB_USERNAME}" --password-stdin ghcr.io + podman push "{{ tag }}" + +# Run with local healthd binary (for testing without an explicit OCI image) (ex. just run-healthd -f test/healthd/debug/compose.yaml up ) +run-healthd compose="examples/immich/compose.yaml" name="immich": build-healthd + go run ./cmd/incus-compose --debug -f {{ compose }} healthd up --recreate --binary bin/ic-healthd + go run ./cmd/incus-compose -f {{ compose }} incus exec {{ name }}-ic-healthd -- tail -n 1000 -f /var/log/ic-healthd.log diff --git a/just/incus.just b/just/incus.just new file mode 100644 index 0000000..2e8e1cb --- /dev/null +++ b/just/incus.just @@ -0,0 +1,153 @@ +# The nested Incus dev environment, and putting it back to a clean state. + +# Run commands in the nested incus. +incus *args: + @echo "Using remote '${INCUS_REMOTE-"local"}': incus $*" >&2 + @incus "$@" + +# Dev install creates your dev environment: `just dev-install [container] [listen] [project] [image]` +dev-install container_name="local:ict" listen='127.0.0.1:1443' project='default' image='images:debian/trixie' storagepool='default' repo='stable': + go install gotest.tools/gotestsum@latest + @just make-nested "{{ container_name }}" "{{ image }}" "{{ listen }}" "{{ project }}" "{{ storagepool }}" "{{ repo }}" + just build-healthd-image + +[private] +make-nested container='local:ict' image='images:debian/trixie' listen="127.0.0.1:1443" project="default" storagepool="default" repo="stable": + #!/usr/bin/env bash + set -euo pipefail + + container="{{ container }}" + image="{{ image }}" + listen="{{ listen }}" + storagepool="{{ storagepool }}" + repo="{{ repo }}" + + key_file="" + cert_file="" + if [[ -f $HOME/.config/incus/client.crt ]] && [[ -f $HOME/.config/incus/client.key ]]; then + key_file="$HOME/.config/incus/client.key" + cert_file="$HOME/.config/incus/client.crt" + fi + + # Run setup script (certificate injection is handled by the script) + set +e + echo "Trying to create a nested container:\n" + INCUS_PROJECT="{{ project }}" ./scripts/setup-nested-incus.sh -c "${cert_file}" -n "${container}" -i "${image}" -r "${repo}" -l "${listen}" -p "${storagepool}" + set -e + + if [[ -z "${listen}" ]]; then + container_ip=$(incus list "${container}" -c4 --format json 2>/dev/null | jq -r '.[0].state.network // {} | [ .[].addresses[]? | select(.family == "inet") | .address ] | .[0] // empty' 2>/dev/null) + if [ -z "${container_ip}" ]; then + echo "Error: Could not get container IP" + echo "This scripts requires you to setup the nested incus instance first, use 'just make-nested' to create it." + exit 1 + fi + + url="https://${container_ip}:8443" + else + url="https://${listen}" + fi + + INCUS_REMOTE="${container%%:*}" incus remote remove "${container##*:}" || true + incus remote add "${container##*:}" "${url}" --accept-certificate + +cleanup: + just purge-projects + just purge-networks || true + sudo systemctl restart incus.service incus.socket + +# Purge all dangling networks (managed and 0 users) from the configured remote +purge-networks: + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + networks=$(incus network list "${remote}:" -f json | jq -r '.[] | select(.used_by | length == 0) | select(.managed == true) | .name') + + if [[ -z "${networks}" ]]; then + echo "No dangling networks found." + exit 1 + fi + + echo "Deleting dangling networks on remote '${remote}':" + while IFS= read -r network; do + echo " Deleting: ${network}" + incus network delete "${remote}:${network}" + done <<< "${networks}" + echo "Done." + +# Removes all images +purge-images *args: + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + images=$(incus image list "${remote}:" {{ args }} -f json | jq -r '.[] .fingerprint') + + if [[ -z "${images}" ]]; then + echo "No images found." + exit 1 + fi + + echo "Deleting images on remote '${remote}':" + while IFS= read -r image; do + echo " Deleting: ${image}" + incus image delete {{ args }} "${remote}:${image}" + done <<< "${images}" + echo "Done." + +# Removes all projects +purge-projects: + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + projects=$(incus project list "${remote}:" -f json | jq -r '.[] .name') + + echo "Deleting projects on remote '${remote}':" + while IFS= read -r project; do + if [[ $project != "default" ]] && [[ $project != "incus-compose-tests-cache" ]] then + echo " Deleting: ${project}" + echo -e "yes\n" | incus project delete -f "${remote}:${project}" + fi + done <<< "${projects}" + echo "Done." + +purge-tokens: + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + tokens=$(incus config trust list-tokens "${remote}:" -f json | jq -r '.[] .client_name') + + if [[ -z "${tokens}" ]]; then + echo "No tokens found." + exit 1 + fi + + echo "Revoking tokens on remote '${remote}':" + while IFS= read -r token; do + echo " Revoking: ${token}" + incus config trust revoke-token "${remote}:${token}" + done <<< "${tokens}" + echo "Done." + +# Removes all trusted client certificates, except the one named "client.crt" +purge-certs: + #!/usr/bin/env bash + set -euo pipefail + + remote="${INCUS_REMOTE:-local}" + certs=$(incus config trust list "${remote}:" -f json | jq -r '.[] | select(.name != "client.crt") | .fingerprint') + + if [[ -z "${certs}" ]]; then + echo "No certificates found." + exit 1 + fi + + echo "Removing certificates on remote '${remote}':" + while IFS= read -r cert; do + echo " Removing: ${cert}" + incus config trust remove "${remote}:${cert}" + done <<< "${certs}" + echo "Done." diff --git a/just/mod.just b/just/mod.just new file mode 100644 index 0000000..0030b52 --- /dev/null +++ b/just/mod.just @@ -0,0 +1,72 @@ +# The module and the gates built on it. +# +# One module today. The loops below exist because the coredns merge brings +# nested modules with it (its benchmarks stay out of the module ecs_view ships +# from), and `go list -m` does not see those from here. + +# List every module directory, one per line. +modules: + @echo "{{ justfile_directory() }}" + +# Lint all files. +lint folder="./...": + shellcheck **/*.sh + npx --yes prettier --check . + golangci-lint run {{ folder }} + +# Lint and fix all files. Imports are gopls' job, not this one - see AGENTS.md. +fix folder="./...": + npx --yes prettier --write . + golangci-lint run --fix {{ folder }} + +# Run `go mod tidy` in every module, reporting every failure rather than the first. +tidy: + #!/usr/bin/env bash + set -uo pipefail + failed=() + for dir in $(just modules); do + echo "==> ${dir#"${PWD}"/}" + go -C "$dir" mod tidy || failed+=("${dir#"${PWD}"/}") + done + if [ ${#failed[@]} -gt 0 ]; then + echo + echo "tidy failed in: ${failed[*]}" >&2 + exit 1 + fi + +# Build every module. Carries on past a failure so a sweep shows all of it. +build-all: + #!/usr/bin/env bash + set -uo pipefail + failed=() + for dir in $(just modules); do + printf '%-38s ' "${dir#"${PWD}"/}" + if out=$(go -C "$dir" build ./... 2>&1); then + echo OK + else + echo FAIL + echo "$out" | sed 's/^/ /' + failed+=("${dir#"${PWD}"/}") + fi + done + if [ ${#failed[@]} -gt 0 ]; then + echo + echo "build failed in: ${failed[*]}" >&2 + exit 1 + fi + +# Run this before you commit/push. +pre-commit: + just tidy + rg -q "// TODO" **/*.go || exit 0 + just lint + just test + +push: pre-commit + git push + +[private] +log-run logfile="" cmd="": + @(time {{ cmd }}) 2>&1 | tee -a {{ logfile }} || EXIT_CODE=$?; \ + echo -e "\n\nCMD: {{ cmd }}\nLog: {{ logfile }}" | tee -a {{ logfile }}; \ + exit ${EXIT_CODE:-0} diff --git a/just/test.just b/just/test.just new file mode 100644 index 0000000..5b27f80 --- /dev/null +++ b/just/test.just @@ -0,0 +1,130 @@ +# Tests. The stages are one implementation plus an environment, see +# CONTRIBUTING.md: the skip helpers read the env vars below, and each wrapper +# sets its own and calls `test`. The wrappers re-invoke just, which is what lets +# a stage override v_test_timeout - the child parses this file with the stage's +# environment already set. + +# How long one run may take. The stage that stands up containers wants more and +# sets it in its own environment before calling us. +v_test_timeout := env("TEST_TIMEOUT", "20m") + +# Run tests against nested Incus, includes direct incus tests. +[env("INCUS_COMPOSE_IMAGE_CACHE", "incus-compose-tests-cache")] +test folder="./..." *args: lint + mkdir -p test/logs + export DATE=`date +%Y%m%d-%H%M%S`; \ + gotestsum --hide-summary=skipped --format testname --jsonfile=test/logs/${DATE}.json --packages={{ folder }} \ + --post-run-command "bash -c 'echo; echo Slowest tests; gotestsum tool slowest --num 10 --jsonfile test/logs/${DATE}.json'" \ + -- -race -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -covermode atomic -coverprofile test/logs/${DATE}-cover.out -v "${@:2}" + +# Run local unit-tests, incus-facing tests are skipped. +[env("INCUS_COMPOSE_TEST_LOCAL", "1")] +test-local folder="./..." *args: + @just test "$@" + +# Run e2e tests. +[env("INCUS_COMPOSE_TEST_E2E", "1")] +[env("TEST_TIMEOUT", "60m")] +test-e2e folder="./..." *args: + @just test "$@" + +# Run example tests. +[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] +test-examples *args: + @just test ./examples/... "$@" + +# Run all tests, includes direct incus, e2 and examples tests. +[env("INCUS_COMPOSE_TEST_E2E", "1")] +[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] +[env("TEST_TIMEOUT", "60m")] +test-all folder="./..." *args: + @just test "$@" + +# Run the tests with a race detector. +test-race folder="./..." *args: + mkdir -p test/logs + gotestsum --hide-summary=skipped --jsonfile=test/logs/`date +%Y%m%d-%H%M%S`.json --packages={{ folder }} -- -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -race -v "${@:2}" + +# Update snapshots for long running tests. +[env("INCUS_COMPOSE_TEST_E2E", "1")] +[env("TEST_TIMEOUT", "60m")] +[env("UPDATE_SNAPSHOTS", "1")] +update-e2e-snapshots folder="./..." *args: + @just test "$@" + +# Update snapshots for examples. +[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] +[env("UPDATE_SNAPSHOTS", "1")] +update-examples-snapshots folder="./..." *args: + @just test "$@" + +# Update snapshot test files that require a remote +[env("UPDATE_SNAPSHOTS", "1")] +update-snapshots folder="./..." *args: + @just test "$@" + +# just test-log # everything the run printed +# just test-log 'FAIL|Error:' # only matching lines (extended regex) +# just test-log '' TestE2EDownNoDeps # only one test's output +# +# On a terminal it follows until Ctrl-C, so it can be started while a run is +# going; piped it reads once and exits. The newest log is picked at startup, so +# a run launched after this is not the one being followed. +[doc("Plain text of the newest test/logs/*.json. Follows on a terminal")] +test-log pattern="" test="": + #!/usr/bin/env bash + set -euo pipefail + + log="" + for candidate in test/logs/*.json; do + [[ -f "${candidate}" ]] || continue + [[ -z "${log}" || "${candidate}" -nt "${log}" ]] && log="${candidate}" + done + + if [[ -z "${log}" ]]; then + echo "no test log yet: run just test first" >&2 + exit 1 + fi + + echo "Log: ${log}" >&2 + + # gotestsum's own lines already end in a newline, so trim jq's. + select='select(.Action == "output")' + if [[ -n "{{ test }}" ]]; then + select="${select} | select(.Test != null and (.Test | startswith(\"{{ test }}\")))" + fi + + # Follow only on a terminal. Piped, `| grep FAIL` would never see EOF and + # hang instead of answering, which is how this gets used from a script. + if [[ -t 1 ]]; then + reader=(tail -n +1 -F "${log}") + else + reader=(cat "${log}") + fi + + set +e + if [[ -z "{{ pattern }}" ]]; then + "${reader[@]}" | jq --unbuffered -r "${select} | .Output | rtrimstr(\"\n\")" + else + "${reader[@]}" | jq --unbuffered -r "${select} | .Output | rtrimstr(\"\n\")" \ + | grep --line-buffered -E "{{ pattern }}" + fi + status=$? + set -e + + # 130 is Ctrl-C, which is the only way out of a follow. + if [[ "${status}" == 130 || "${status}" == 2 ]]; then + exit 0 + fi + + # 141 is a closed pipe, which is what `just test-log | head` looks like. + if [[ "${status}" == 141 ]]; then + exit 0 + fi + + if [[ "${status}" == 1 && -n "{{ pattern }}" ]]; then + echo "nothing matching '{{ pattern }}'" >&2 + exit 0 + fi + + exit "${status}" diff --git a/justfile b/justfile index a9afc69..a8be1f5 100644 --- a/justfile +++ b/justfile @@ -8,6 +8,9 @@ # Local development (uses your real Incus - be careful!): # just run-local # just test-local +# +# Recipes live in just/*.just by topic. Everything the coredns merge brings is +# an import of its own, so it lands here without touching these. set dotenv-load set shell := ["bash", "-euo", "pipefail", "-c"] @@ -17,366 +20,11 @@ set positional-arguments v_test_procs := env("TEST_PROCS", "2") +import 'just/mod.just' +import 'just/test.just' +import 'just/build.just' +import 'just/incus.just' + [private] default: @just --list - -cleanup: - just purge-projects - just purge-networks || true - sudo systemctl restart incus.service incus.socket - -# Run tests against nested Incus, includes direct incus tests. -[env("INCUS_COMPOSE_IMAGE_CACHE", "incus-compose-tests-cache")] -test folder="./..." *args: lint - export DATE=`date +%Y%m%d-%H%M%S`; \ - gotestsum --hide-summary=skipped --format testname --jsonfile=test/logs/${DATE}.json --packages={{ folder }} \ - --post-run-command "bash -c 'echo; echo Slowest tests; gotestsum tool slowest --num 10 --jsonfile test/logs/${DATE}.json'" \ - -- -race -parallel {{ v_test_procs }} -timeout 20m -covermode atomic -coverprofile test/logs/${DATE}-cover.out -v "${@:2}"; \ - -# Run local unit-tests, incus-facing tests are skipped. -[env("INCUS_COMPOSE_TEST_LOCAL", "1")] -test-local folder="./..." *args: - @just test "$@" - -# Run e2e tests. -[env("INCUS_COMPOSE_TEST_E2E", "1")] -test-e2e folder="./..." *args: - @just test "$@" - -# Run example tests. -[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] -test-examples *args: - @just test ./examples/... "$@" - -# Run all tests, includes direct incus, e2 and examples tests. -[env("INCUS_COMPOSE_TEST_E2E", "1")] -[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] -test-all folder="./..." *args: - @just test "$@" - -# Run the tests with a race detector. -test-race folder="./..." *args: - gotestsum --hide-summary=skipped --jsonfile=test/logs/`date +%Y%m%d-%H%M%S`.json --packages={{ folder }} -- -parallel {{ v_test_procs }} -timeout 20m -race -v "${@:2}" - -# Update snapshots for long running tests. -[env("INCUS_COMPOSE_TEST_E2E", "1")] -[env("UPDATE_SNAPSHOTS", "1")] -update-e2e-snapshots folder="./..." *args: - @just test "$@" - -# Update snapshots for examples. -[env("INCUS_COMPOSE_TEST_EXAMPLES", "1")] -[env("UPDATE_SNAPSHOTS", "1")] -update-examples-snapshots folder="./..." *args: - @just test "$@" - -# Update snapshot test files that require a remote -[env("UPDATE_SNAPSHOTS", "1")] -update-snapshots folder="./..." *args: - @just test "$@" - -# just test-log # everything the run printed -# just test-log 'FAIL|Error:' # only matching lines (extended regex) -# just test-log '' TestE2EDownNoDeps # only one test's output -[doc("Plain text of the newest test/logs/*.json, for grepping")] -test-log pattern="" test="": - #!/usr/bin/env bash - set -euo pipefail - - log="" - for candidate in test/logs/*.json; do - [[ -f "${candidate}" ]] || continue - [[ -z "${log}" || "${candidate}" -nt "${log}" ]] && log="${candidate}" - done - - if [[ -z "${log}" ]]; then - echo "no test log yet: run just test first" >&2 - exit 1 - fi - - echo "Log: ${log}" >&2 - - # gotestsum's own lines already end in a newline, so trim jq's. - select='select(.Action == "output")' - if [[ -n "{{ test }}" ]]; then - select="${select} | select(.Test != null and (.Test | startswith(\"{{ test }}\")))" - fi - - set +e - if [[ -z "{{ pattern }}" ]]; then - jq -r "${select} | .Output | rtrimstr(\"\n\")" "${log}" - else - jq -r "${select} | .Output | rtrimstr(\"\n\")" "${log}" | grep -E "{{ pattern }}" - fi - status=$? - set -e - - # 141 is a closed pipe, which is what `just test-log | head` looks like. - if [[ "${status}" == 141 ]]; then - exit 0 - fi - - if [[ "${status}" == 1 && -n "{{ pattern }}" ]]; then - echo "nothing matching '{{ pattern }}'" >&2 - exit 0 - fi - - exit "${status}" - -[private] -log-run logfile="" cmd="": - @(time {{ cmd }}) 2>&1 | tee -a {{ logfile }} || EXIT_CODE=$?; \ - echo -e "\n\nCMD: {{ cmd }}\nLog: {{ logfile }}" | tee -a {{ logfile }}; \ - exit ${EXIT_CODE:-0} - -# Lint all files. -lint folder="./...": - shellcheck **/*.sh - npx --yes prettier --check . - golangci-lint run {{ folder }} - -# Lint and fix all files. Imports are gopls' job, not this one - see AGENTS.md. -fix folder="./...": - npx --yes prettier --write . - golangci-lint run --fix {{ folder }} - -# Dev install creates your dev environment: `just dev-install [container] [listen] [project] [image]` -dev-install container_name="local:ict" listen='127.0.0.1:1443' project='default' image='images:debian/trixie' storagepool='default' repo='stable': - go install gotest.tools/gotestsum@latest - @just make-nested "{{ container_name }}" "{{ image }}" "{{ listen }}" "{{ project }}" "{{ storagepool }}" "{{ repo }}" - just build-healthd-image - -# Run commands in the nested incus. -incus *args: - @echo "Using remote '${INCUS_REMOTE-"local"}': incus $*" >&2 - @incus "$@" - -# Build ic-healthd binary -build-healthd: - CGO_ENABLED=0 go build -tags=netgo -ldflags="-w -s -X github.com/lxc/incus-compose/cmd/ic-healthd/version.Version=`git describe --tags --always --long --dirty="-dirty"`" -trimpath -o bin/ic-healthd ./cmd/ic-healthd - -# Build ic-healthd container image -build-healthd-image tag_base="ghcr.io/lxc/incus-compose/ic-healthd": - #!/usr/bin/env bash - set -euo pipefail - - if [[ ! -f .env ]]; then - cp .env.sample .env - fi - - # The random suffix gives a clear sign that your version is running. - VERSION="`git describe --tags --always --long --dirty="-dirty"`-`openssl rand -hex 4`" - # Container tags carry no v prefix, `release` pushes them without one too. - export VERSION="${VERSION#v}" - echo ${VERSION} - - echo "Building for the 'default' cache on '${INCUS_REMOTE}'" - just run -P cmd/ic-healthd build --os-env # os-env cause of VERSION - - if [[ ${INCUS_COMPOSE_IMAGE_CACHE:-} != "incus-compose-tests-cache" ]]; then - echo "Building for the 'incus-compose-tests-cache' cache on '${INCUS_REMOTE}'" - just run --image-cache="incus-compose-tests-cache" -P cmd/ic-healthd build --os-env - fi - - sed -i -e 's|export INCUS_COMPOSE_HEALTHD_IMAGE=".*"|export INCUS_COMPOSE_HEALTHD_IMAGE="{{ tag_base }}:'${VERSION}'"|g' .env - -# Rebuild the ic-healthd image and put the shared daemon on it -update-healthd *args="--trace": build-healthd-image - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - - echo "Deleting the global ic-healthd on remote '${remote}'" - echo "yes" | incus project rm --force "incus-compose" || true - - # New image - export INCUS_COMPOSE_HEALTHD_IMAGE=$(source .env; echo "$INCUS_COMPOSE_HEALTHD_IMAGE") - echo "Image ${INCUS_COMPOSE_HEALTHD_IMAGE}" - - # The healthd-scope project is left behind as the handle for `healthd logs`. - just run healthd up {{ args }} - -# Build a dev binary -build: update-healthd - #!/usr/bin/env bash - set -euo pipefail - - image=$(source .env; echo "${INCUS_COMPOSE_HEALTHD_IMAGE:-}") - version="${image##*:}" - - if [[ "${version}" == "${image}" ]]; then - version="`git describe --tags --always --long --dirty="-dirty"`" - fi - - go build -ldflags="-X github.com/lxc/incus-compose/cmd/incus-compose/version.Version=v${version#v}" -o bin/incus-compose ./cmd/incus-compose - -# Build ic-healthd container image -release-healthd-image tag="ghcr.io/lxc/incus-compose/ic-healthd:latest": - #!/usr/bin/env bash - set -euo pipefail - - VERSION="`git describe --tags --always --long --dirty="-dirty"`-`openssl rand -hex 4`" - - # New image - podman build --tag "{{ tag }}" --build-arg VERSION="${VERSION}" -f ./cmd/ic-healthd/Dockerfile . - echo "Image ${INCUS_COMPOSE_HEALTHD_IMAGE}" - - echo "${GITHUB_TOKEN}" | podman login --username "${GITHUB_USERNAME}" --password-stdin ghcr.io - podman push "{{ tag }}" - -# Run with local healthd binary (for testing without an explicit OCI image) (ex. just run-healthd -f test/healthd/debug/compose.yaml up ) -run-healthd compose="examples/immich/compose.yaml" name="immich": build-healthd - go run ./cmd/incus-compose --debug -f {{ compose }} healthd up --recreate --binary bin/ic-healthd - go run ./cmd/incus-compose -f {{ compose }} incus exec {{ name }}-ic-healthd -- tail -n 1000 -f /var/log/ic-healthd.log - -# Usage: just run -f test/fixtures/simple/compose.yaml config -run *args: - @go run ./cmd/incus-compose {{ args }} - -# Purge all dangling networks (managed and 0 users) from the configured remote -purge-networks: - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - networks=$(incus network list "${remote}:" -f json | jq -r '.[] | select(.used_by | length == 0) | select(.managed == true) | .name') - - if [[ -z "${networks}" ]]; then - echo "No dangling networks found." - exit 1 - fi - - echo "Deleting dangling networks on remote '${remote}':" - while IFS= read -r network; do - echo " Deleting: ${network}" - incus network delete "${remote}:${network}" - done <<< "${networks}" - echo "Done." - -# Removes all images -purge-images *args: - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - images=$(incus image list "${remote}:" {{ args }} -f json | jq -r '.[] .fingerprint') - - if [[ -z "${images}" ]]; then - echo "No images found." - exit 1 - fi - - echo "Deleting images on remote '${remote}':" - while IFS= read -r image; do - echo " Deleting: ${image}" - incus image delete {{ args }} "${remote}:${image}" - done <<< "${images}" - echo "Done." - -# Removes all projects -purge-projects: - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - projects=$(incus project list "${remote}:" -f json | jq -r '.[] .name') - - echo "Deleting projects on remote '${remote}':" - while IFS= read -r project; do - if [[ $project != "default" ]] && [[ $project != "incus-compose-tests-cache" ]] then - echo " Deleting: ${project}" - echo -e "yes\n" | incus project delete -f "${remote}:${project}" - fi - done <<< "${projects}" - echo "Done." - -purge-tokens: - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - tokens=$(incus config trust list-tokens "${remote}:" -f json | jq -r '.[] .client_name') - - if [[ -z "${tokens}" ]]; then - echo "No tokens found." - exit 1 - fi - - echo "Revoking tokens on remote '${remote}':" - while IFS= read -r token; do - echo " Revoking: ${token}" - incus config trust revoke-token "${remote}:${token}" - done <<< "${tokens}" - echo "Done." - -# Removes all trusted client certificates, except the one named "client.crt" -purge-certs: - #!/usr/bin/env bash - set -euo pipefail - - remote="${INCUS_REMOTE:-local}" - certs=$(incus config trust list "${remote}:" -f json | jq -r '.[] | select(.name != "client.crt") | .fingerprint') - - if [[ -z "${certs}" ]]; then - echo "No certificates found." - exit 1 - fi - - echo "Removing certificates on remote '${remote}':" - while IFS= read -r cert; do - echo " Removing: ${cert}" - incus config trust remove "${remote}:${cert}" - done <<< "${certs}" - echo "Done." - -# Run this before you commit/push. -pre-commit: - go mod tidy - rg -q "// TODO" **/*.go || exit 0 - just lint - just test - -push: pre-commit - git push - -[private] -make-nested container='local:ict' image='images:debian/trixie' listen="127.0.0.1:1443" project="default" storagepool="default" repo="stable": - #!/usr/bin/env bash - set -euo pipefail - - container="{{ container }}" - image="{{ image }}" - listen="{{ listen }}" - storagepool="{{ storagepool }}" - repo="{{ repo }}" - - key_file="" - cert_file="" - if [[ -f $HOME/.config/incus/client.crt ]] && [[ -f $HOME/.config/incus/client.key ]]; then - key_file="$HOME/.config/incus/client.key" - cert_file="$HOME/.config/incus/client.crt" - fi - - # Run setup script (certificate injection is handled by the script) - set +e - echo "Trying to create a nested container:\n" - INCUS_PROJECT="{{ project }}" ./scripts/setup-nested-incus.sh -c "${cert_file}" -n "${container}" -i "${image}" -r "${repo}" -l "${listen}" -p "${storagepool}" - set -e - - if [[ -z "${listen}" ]]; then - container_ip=$(incus list "${container}" -c4 --format json 2>/dev/null | jq -r '.[0].state.network // {} | [ .[].addresses[]? | select(.family == "inet") | .address ] | .[0] // empty' 2>/dev/null) - if [ -z "${container_ip}" ]; then - echo "Error: Could not get container IP" - echo "This scripts requires you to setup the nested incus instance first, use 'just make-nested' to create it." - exit 1 - fi - - url="https://${container_ip}:8443" - else - url="https://${listen}" - fi - - INCUS_REMOTE="${container%%:*}" incus remote remove "${container##*:}" || true - incus remote add "${container##*:}" "${url}" --accept-certificate From e55da1addcb72a2e143dc3a5369d61b71fdcd4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Wed, 12 Aug 2026 11:28:52 +0200 Subject: [PATCH 2/5] feat(tests): share the tier guards and test helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Jochum --- client/client_test.go | 29 ++--- client/resource_image_test.go | 57 ++++----- client/resource_instance_test.go | 11 +- client/resource_network_test.go | 22 ++-- client/resource_profile_test.go | 16 +-- client/resource_storage_volume_lock_test.go | 20 +-- client/resource_storage_volume_test.go | 20 +-- client/stack_test.go | 20 +-- cmd/ic-healthd/e2e_test.go | 60 +++++---- cmd/ic-healthd/healthd_test.go | 7 -- cmd/ic-healthd/instance_actions_test.go | 21 ++-- cmd/ic-healthd/main_test.go | 3 +- cmd/incus-compose/bind_mount_test.go | 9 +- cmd/incus-compose/build_test.go | 54 ++++----- cmd/incus-compose/e2e_test.go | 117 +++++++++--------- cmd/incus-compose/exec_test.go | 10 +- cmd/incus-compose/healthd_e2e_test.go | 31 ++--- cmd/incus-compose/healthd_test.go | 13 +- cmd/incus-compose/main_test.go | 71 +++-------- cmd/incus-compose/nat_proxy_test.go | 17 +-- cmd/incus-compose/pull_test.go | 20 +-- cmd/incus-compose/regression_test.go | 11 +- cmd/incus-compose/up_test.go | 3 +- cmd/incus-compose/wellknown_test.go | 10 +- examples/examples_test.go | 31 +---- iclient/incus_busy_test.go | 4 +- iclient/incus_console_test.go | 4 +- iclient/incus_events_test.go | 4 +- iclient/incus_exec_test.go | 4 +- iclient/incus_image_test.go | 6 +- iclient/incus_network_test.go | 4 +- iclient/incus_operation_test.go | 4 +- iclient/incus_profile_test.go | 4 +- iclient/incus_server_test.go | 4 +- iclient/incus_sftp_test.go | 6 +- iclient/incus_test.go | 30 ++--- project/instance_test.go | 11 +- project/project_test.go | 7 -- testlib/compose.go | 15 +++ testlib/doc.go | 9 ++ testlib/files.go | 25 ++++ testlib/incus.go | 128 ++++++++++++++++++++ testlib/snapshot.go | 42 +++++++ testlib/tier.go | 43 +++++++ testlib/tier_test.go | 72 +++++++++++ 45 files changed, 689 insertions(+), 420 deletions(-) create mode 100644 testlib/compose.go create mode 100644 testlib/doc.go create mode 100644 testlib/files.go create mode 100644 testlib/incus.go create mode 100644 testlib/snapshot.go create mode 100644 testlib/tier.go create mode 100644 testlib/tier_test.go diff --git a/client/client_test.go b/client/client_test.go index edcf69a..45dae90 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func TestMain(m *testing.M) { @@ -24,13 +26,6 @@ func TestMain(m *testing.M) { os.Exit(code) } -func skipLocal(t *testing.T) { - t.Helper() - if os.Getenv("INCUS_COMPOSE_TEST_LOCAL") != "" { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_LOCAL is set, run `just test` for this test") - } -} - // newRandomTestClient creates a GlobalClient, a fresh project-scoped Client, // and registers t.Cleanup to delete the project on teardown. func newRandomTestClient(_ context.Context, t *testing.T, prefix string) *Client { @@ -145,7 +140,7 @@ func TestSanitizeProjectName(t *testing.T) { func TestClientConnection_IsConnected(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -154,7 +149,7 @@ func TestClientConnection_IsConnected(t *testing.T) { func TestClientProject_GlobalClientKeepsDefaultProfile(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -180,7 +175,7 @@ func TestClientProject_GlobalClientKeepsDefaultProfile(t *testing.T) { func TestClientProject_ImageCacheIsInCacheProfile(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -196,7 +191,7 @@ func TestClientProject_ImageCacheIsInCacheProfile(t *testing.T) { func TestClientProject_EnsureWithCreate(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -210,7 +205,7 @@ func TestClientProject_EnsureWithCreate(t *testing.T) { func TestClientProject_EnsureWithoutCreate_Fails(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -222,7 +217,7 @@ func TestClientProject_EnsureWithoutCreate_Fails(t *testing.T) { func TestClientProject_NameIsPreserved(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -236,7 +231,7 @@ func TestClientProject_NameIsPreserved(t *testing.T) { func TestClientProject_NameIsSanitized(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -252,7 +247,7 @@ func TestClientProject_NameIsSanitized(t *testing.T) { func TestClientProject_EnsureIdempotent(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -268,7 +263,7 @@ func TestClientProject_EnsureIdempotent(t *testing.T) { func TestClientProject_DeleteSucceeds(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) @@ -282,7 +277,7 @@ func TestClientProject_DeleteSucceeds(t *testing.T) { func TestClientProject_DeleteNonExistent_NoError(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() gc, err := NewTestClient(ctx) require.NoError(t, err) diff --git a/client/resource_image_test.go b/client/resource_image_test.go index c99731f..8ab6c30 100644 --- a/client/resource_image_test.go +++ b/client/resource_image_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/iclient" + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -181,7 +182,7 @@ func TestImageConfig_RemoteAndImageParsed(t *testing.T) { func TestImageEnsure(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -252,7 +253,7 @@ func TestImageEnsure(t *testing.T) { func TestImageEnsure_Idempotent(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-idempotent-") @@ -268,7 +269,7 @@ func TestImageEnsure_Idempotent(t *testing.T) { func TestImageEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-retry-") @@ -286,7 +287,7 @@ func TestImageEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { func TestImageEnsure_ExistingImage_NewResource(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-existing-") @@ -308,7 +309,7 @@ func TestImageEnsure_ExistingImage_NewResource(t *testing.T) { func TestImageEnsure_ExistsOnNewClient(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-persist-") @@ -332,7 +333,7 @@ func TestImageEnsure_ExistsOnNewClient(t *testing.T) { func TestImageDelete(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -373,7 +374,7 @@ func TestImageDelete(t *testing.T) { func TestImageDelete_NotEnsured_NoError(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-delne-") @@ -389,7 +390,7 @@ func TestImageDelete_NotEnsured_NoError(t *testing.T) { func TestImageProperties(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-props-") @@ -408,7 +409,7 @@ func TestImageProperties(t *testing.T) { func TestImageFromCache(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-from-cache-") @@ -431,7 +432,7 @@ func TestImageFromCache(t *testing.T) { func TestImagePullNever_StoreHit(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-pull-never-hit-") @@ -452,7 +453,7 @@ func TestImagePullNever_StoreHit(t *testing.T) { func TestImagePullNever_StoreMiss(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-pull-never-miss-") @@ -468,7 +469,7 @@ func TestImagePullNever_StoreMiss(t *testing.T) { func TestImageBuild_StoreHitSkipsBuilder(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // Seed the cache under the alias the build would produce. @@ -504,7 +505,7 @@ func lockableImage(t *testing.T, c *Client, name string) *Image { } func TestImageLockStore_SameAliasSerializes(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() name := "docker.io/library/ic-lock-" + strings.ToLower(RandString(8)) + ":latest" @@ -540,7 +541,7 @@ func TestImageLockStore_SameAliasSerializes(t *testing.T) { } func TestImageLockStore_DifferentAliasesDoNotBlock(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() suffix := strings.ToLower(RandString(8)) @@ -569,7 +570,7 @@ func TestImageLockStore_DifferentAliasesDoNotBlock(t *testing.T) { } func TestImageEnsure_ConcurrentSameImage(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() const workers = 4 @@ -605,7 +606,7 @@ func TestImageEnsure_ConcurrentSameImage(t *testing.T) { } func TestImageEnsure_ConcurrentDifferentImages(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() names := []string{ @@ -649,7 +650,7 @@ func TestImageEnsure_ConcurrentDifferentImages(t *testing.T) { } func TestImageEnsure_ProjectCopySurvivesCacheDeletion(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-cache-pruned-") @@ -684,7 +685,7 @@ func TestImageEnsure_ProjectCopySurvivesCacheDeletion(t *testing.T) { } func TestImagePullNever_NoCacheStoreMiss(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-never-nocache-") c.imageCache = nil @@ -698,7 +699,7 @@ func TestImagePullNever_NoCacheStoreMiss(t *testing.T) { } func TestImageCreateDirect_NoSource(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-nosource-") c.imageCache = nil @@ -711,7 +712,7 @@ func TestImageCreateDirect_NoSource(t *testing.T) { } func TestImageLockStore_NoCacheIsNoop(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-lock-nocache-") c.imageCache = nil @@ -726,7 +727,7 @@ func TestImageLockStore_NoCacheIsNoop(t *testing.T) { } func TestImageLockStore_CustomVolumeIsSeparate(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() name := "docker.io/library/ic-lockvol-" + strings.ToLower(RandString(8)) + ":latest" @@ -765,7 +766,7 @@ func TestImageLockStore_CustomVolumeIsSeparate(t *testing.T) { } func TestImageLockStore_ConcurrentVolumeCreate(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // A volume name nothing has created yet, so every worker races to make it. @@ -817,7 +818,7 @@ func TestImageLockStore_ConcurrentVolumeCreate(t *testing.T) { } func TestImageBuild_NeverErrors(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-build-never-") @@ -832,7 +833,7 @@ func TestImageBuild_NeverErrors(t *testing.T) { } func TestImageBuild_WithoutCreateDoesNotBuild(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-build-nocreate-") @@ -848,7 +849,7 @@ func TestImageBuild_WithoutCreateDoesNotBuild(t *testing.T) { } func TestImageBuild_ForceIgnoresStoreHit(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // Seed the cache under the alias a build would produce. @@ -872,7 +873,7 @@ func TestImageBuild_ForceIgnoresStoreHit(t *testing.T) { func TestImageNoCache(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "image-no-cache-") c.imageCache = nil @@ -885,7 +886,7 @@ func TestImageNoCache(t *testing.T) { func TestImagePullDeletes(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "image-pull-delete-") r, err := c.Resource(KindImage, "docker.io/library/alpine:3.22", &ImageConfig{}) @@ -912,7 +913,7 @@ func TestImagePullDeletes(t *testing.T) { func TestImageHooks(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { diff --git a/client/resource_instance_test.go b/client/resource_instance_test.go index 58c246c..d16fed9 100644 --- a/client/resource_instance_test.go +++ b/client/resource_instance_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -129,7 +130,7 @@ func TestResolveEntrypoint(t *testing.T) { // values: a missing one is added, an existing one keeps what it holds. func TestInstanceEnsureAddsMissingConfigOnly(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "ensure-addmissing-") @@ -184,7 +185,7 @@ func TestInstanceEnsureAddsMissingConfigOnly(t *testing.T) { // SetHealthCheckingStopped relies on. func TestInstanceConfigPatchOnlyTouchesNamedKeys(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "patch-config-") @@ -242,7 +243,7 @@ func TestInstanceConfigPatchOnlyTouchesNamedKeys(t *testing.T) { // are kept fresh by the project client's listener; nothing else wakes them. func TestCloneInstancesFollowLifecycleEvents(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "clone-events-") @@ -288,7 +289,7 @@ func TestCloneInstancesFollowLifecycleEvents(t *testing.T) { // writes the intent marker ic-healthd reads, and nothing else. func TestInstanceStoppedLeavesTheStatusAlone(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stopped-status-") @@ -341,7 +342,7 @@ func TestInstanceStoppedLeavesTheStatusAlone(t *testing.T) { // with no daemon to report, the instance says so. func TestInstanceWithoutHealthdReportsUnknown(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "nohealthd-status-") diff --git a/client/resource_network_test.go b/client/resource_network_test.go index 8df64e7..154beb7 100644 --- a/client/resource_network_test.go +++ b/client/resource_network_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -313,7 +315,7 @@ func TestNetworkExternal_InitialIncusNameIsRaw(t *testing.T) { func TestNetworkEnsure(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -392,7 +394,7 @@ func TestNetworkEnsure(t *testing.T) { func TestNetworkEnsure_Idempotent(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-idempotent-") @@ -408,7 +410,7 @@ func TestNetworkEnsure_Idempotent(t *testing.T) { func TestNetworkEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-retry-") @@ -426,7 +428,7 @@ func TestNetworkEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { func TestNetworkEnsure_ExistsOnNewClient(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-persist-") @@ -446,7 +448,7 @@ func TestNetworkEnsure_ExistsOnNewClient(t *testing.T) { func TestNetworkProjectDeletesNetwork(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-projdel-") @@ -474,7 +476,7 @@ func TestNetworkProjectDeletesNetwork(t *testing.T) { func TestNetworkDelete(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -522,7 +524,7 @@ func TestNetworkDelete(t *testing.T) { func TestNetworkHooks(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -647,7 +649,7 @@ func TestNetworkHooks(t *testing.T) { func TestNetworkExternal_EnsureFailsIfNotExists(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-ext-") @@ -662,7 +664,7 @@ func TestNetworkExternal_EnsureFailsIfNotExists(t *testing.T) { func TestNetworkExternal_DeleteIsNoOp(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "network-extdel-") @@ -804,7 +806,7 @@ func TestCalcIPv6DHCPRange(t *testing.T) { } func TestNetworkEnsure_ConcurrentCreate(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // OverrideName skips the per-project prefix, so every worker resolves to diff --git a/client/resource_profile_test.go b/client/resource_profile_test.go index 435798b..3d5de16 100644 --- a/client/resource_profile_test.go +++ b/client/resource_profile_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -58,7 +60,7 @@ func TestProfileIncusName_Sanitized(t *testing.T) { func TestProfileEnsure(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -131,7 +133,7 @@ func TestProfileEnsure(t *testing.T) { func TestProfileEnsure_Idempotent(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "profile-idempotent-") @@ -147,7 +149,7 @@ func TestProfileEnsure_Idempotent(t *testing.T) { func TestProfileEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "profile-retry-") @@ -165,7 +167,7 @@ func TestProfileEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { func TestProfileEnsure_ExistsOnNewClient(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "profile-persist-") @@ -189,7 +191,7 @@ func TestProfileEnsure_ExistsOnNewClient(t *testing.T) { func TestProfileDelete(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -230,7 +232,7 @@ func TestProfileDelete(t *testing.T) { func TestProfileHooks(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -384,7 +386,7 @@ func TestProfileHooks(t *testing.T) { } func TestProfileEnsure_ConcurrentCreate(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // One project, so every worker races for the same profile. diff --git a/client/resource_storage_volume_lock_test.go b/client/resource_storage_volume_lock_test.go index 10d1745..b40b491 100644 --- a/client/resource_storage_volume_lock_test.go +++ b/client/resource_storage_volume_lock_test.go @@ -10,6 +10,8 @@ import ( "github.com/pkg/sftp" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func ensuredLockTestVolume(t *testing.T, c *Client, name string) *StorageVolume { @@ -36,7 +38,7 @@ func lockTestSFTP(t *testing.T, vol *StorageVolume) *sftp.Client { func TestStorageVolumeLock_NotEnsured(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-notensured-") r, err := c.Resource(KindStorageVolume, "unensured", &StorageVolumeConfig{}) @@ -53,7 +55,7 @@ func TestStorageVolumeLock_NotEnsured(t *testing.T) { func TestStorageVolumeLock_AcquireExcludesAndUnlockRemoves(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-") vol := ensuredLockTestVolume(t, c, "locked-vol") @@ -76,7 +78,7 @@ func TestStorageVolumeLock_AcquireExcludesAndUnlockRemoves(t *testing.T) { func TestStorageVolumeLock_NestedNameCreatesParents(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-nested-") vol := ensuredLockTestVolume(t, c, "nested-vol") @@ -105,7 +107,7 @@ func TestStorageVolumeLock_NestedNameCreatesParents(t *testing.T) { func TestStorageVolumeLock_MultipleLocksInOneVolume(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-multi-") vol := ensuredLockTestVolume(t, c, "multi-vol") @@ -145,7 +147,7 @@ func TestStorageVolumeLock_MultipleLocksInOneVolume(t *testing.T) { func TestStorageVolumeLock_ConcurrentDistinctLocks(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-concurrent-") vol := ensuredLockTestVolume(t, c, "concurrent-vol") @@ -204,7 +206,7 @@ func TestStorageVolumeLock_ConcurrentDistinctLocks(t *testing.T) { func TestStorageVolumeLock_BlocksUntilContextDone(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-block-") vol := ensuredLockTestVolume(t, c, "blocked-vol") @@ -231,7 +233,7 @@ func TestStorageVolumeLock_BlocksUntilContextDone(t *testing.T) { func TestStorageVolumeLock_StaleTakeoverAfterCrash(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-stale-") vol := ensuredLockTestVolume(t, c, "stale-vol") @@ -260,7 +262,7 @@ func TestStorageVolumeLock_StaleTakeoverAfterCrash(t *testing.T) { func TestStorageVolumeLock_HeartbeatPreventsStaleTakeover(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-heartbeat-") vol := ensuredLockTestVolume(t, c, "heartbeat-vol") @@ -287,7 +289,7 @@ func TestStorageVolumeLock_HeartbeatPreventsStaleTakeover(t *testing.T) { func TestStorageVolumeLock_UnlockDeletesOnlyOwnLock(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := newRandomTestClient(t.Context(), t, "volume-lock-owner-") vol := ensuredLockTestVolume(t, c, "owner-vol") diff --git a/client/resource_storage_volume_test.go b/client/resource_storage_volume_test.go index 42d0c3d..560fa9a 100644 --- a/client/resource_storage_volume_test.go +++ b/client/resource_storage_volume_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -82,7 +84,7 @@ func TestStorageVolumeConfig_CustomPool(t *testing.T) { func TestStorageVolumeEnsure(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -163,7 +165,7 @@ func TestStorageVolumeEnsure(t *testing.T) { func TestStorageVolumeEnsure_Idempotent(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "volume-idempotent-") @@ -179,7 +181,7 @@ func TestStorageVolumeEnsure_Idempotent(t *testing.T) { func TestStorageVolumeEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "volume-retry-") @@ -197,7 +199,7 @@ func TestStorageVolumeEnsure_WithoutCreate_ThenWithCreate(t *testing.T) { func TestStorageVolumeEnsure_ShiftedVolume_Start(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "volume-shifted-") @@ -214,7 +216,7 @@ func TestStorageVolumeEnsure_ShiftedVolume_Start(t *testing.T) { func TestStorageVolumeEnsure_HealthdShiftedVolume(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "volume-healthd-") @@ -240,7 +242,7 @@ func TestStorageVolumeEnsure_HealthdShiftedVolume(t *testing.T) { func TestStorageVolumeEnsure_ExistsOnNewClient(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "volume-persist-") @@ -264,7 +266,7 @@ func TestStorageVolumeEnsure_ExistsOnNewClient(t *testing.T) { func TestStorageVolumeDelete(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -305,7 +307,7 @@ func TestStorageVolumeDelete(t *testing.T) { func TestStorageVolumeHooks(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() tests := []struct { @@ -425,7 +427,7 @@ func TestStorageVolumeHooks(t *testing.T) { } func TestStorageVolumeEnsure_ConcurrentCreate(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() // One project, one volume name, so every worker races to create it. diff --git a/client/stack_test.go b/client/stack_test.go index 769b37a..5a6e86c 100644 --- a/client/stack_test.go +++ b/client/stack_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // ---------------------------------------------------------------------------- @@ -111,7 +113,7 @@ func TestAddDeduplicatesSamePointer(t *testing.T) { // Uses tiny busybox variants to minimize bandwidth. func TestParallelImageDownload(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-parallel-") @@ -145,7 +147,7 @@ func TestParallelImageDownload(t *testing.T) { func TestStackHooksWithStack(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-hooks-") @@ -184,7 +186,7 @@ func TestStackHooksWithStack(t *testing.T) { func TestStackErrorAggregation(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-erragg-") @@ -206,7 +208,7 @@ func TestStackErrorAggregation(t *testing.T) { func TestStackInstanceWithSecrets(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-secrets-") @@ -264,7 +266,7 @@ func TestStackInstanceWithSecrets(t *testing.T) { func TestStackEnsureWithoutCreate_Fails(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-nocreate-") @@ -278,7 +280,7 @@ func TestStackEnsureWithoutCreate_Fails(t *testing.T) { func TestStackSingleProfileEnsure(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-profile-") @@ -297,7 +299,7 @@ func TestStackSingleProfileEnsure(t *testing.T) { func TestStackProfileAndNetworkMixedPriorities(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-mixed-") @@ -319,7 +321,7 @@ func TestStackProfileAndNetworkMixedPriorities(t *testing.T) { func TestStackSimple(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-simple-") @@ -362,7 +364,7 @@ func TestStackSimple(t *testing.T) { func TestStackScale(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() c := newRandomTestClient(ctx, t, "stack-scale-") diff --git a/cmd/ic-healthd/e2e_test.go b/cmd/ic-healthd/e2e_test.go index be247b1..11e4cca 100644 --- a/cmd/ic-healthd/e2e_test.go +++ b/cmd/ic-healthd/e2e_test.go @@ -17,15 +17,9 @@ import ( "github.com/lxc/incus-compose/client" "github.com/lxc/incus-compose/iclient" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) -func skipE2E(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_E2E") - if !ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_E2E is not set, run `just test-e2e` for this test") - } -} - // newToken mints the trust token incus-compose hands the sidecar. Incus takes // the name and the scope from here, so projects is what bounds the daemon; // empty mints an unrestricted token. @@ -240,8 +234,8 @@ func setState(t *testing.T, conn *iclient.Connection, name string, req incusApi. // the one-time token, persist the pair, and come back without a token. func TestE2EConnectRegistersThenReuses(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-connect-") @@ -272,8 +266,8 @@ func TestE2EConnectRegistersThenReuses(t *testing.T) { // token: a file in secrets-dir, not the environment. func TestE2EConnectRegistersFromATokenFile(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-connect-file-") @@ -296,8 +290,8 @@ func TestE2EConnectRegistersFromATokenFile(t *testing.T) { // sidecar from looking healthy while it can do nothing. func TestE2EConnectWithoutTokenOrCert(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-connect-bare-") @@ -312,8 +306,8 @@ func TestE2EConnectWithoutTokenOrCert(t *testing.T) { // depends_on: { condition: service_healthy }. func TestE2ESchedulerReportsHealthy(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-healthy-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -332,8 +326,8 @@ func TestE2ESchedulerReportsHealthy(t *testing.T) { // a verdict: the status only turns after the configured run of failures. func TestE2ESchedulerReportsUnhealthyAfterRetries(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-unhealthy-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -353,8 +347,8 @@ func TestE2ESchedulerReportsUnhealthyAfterRetries(t *testing.T) { // raw API stop leaves no intent marker, so the restart policy applies. func TestE2ESchedulerRestartsACrashedInstance(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-crash-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -382,8 +376,8 @@ func TestE2ESchedulerRestartsACrashedInstance(t *testing.T) { // lifecycle listener, and a verdict in each project. func TestE2EMultipleProjects(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) one := testProject(t, "healthd-multi-a-") two := testProject(t, "healthd-multi-b-") @@ -409,8 +403,8 @@ func TestE2EMultipleProjects(t *testing.T) { // the reload is only repaired if that scheduler is alive and still fed. func TestE2EReloadKeepsWatching(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-reload-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -441,8 +435,8 @@ func TestE2EReloadKeepsWatching(t *testing.T) { // what carries the marker and leaves everything else alone. func TestE2EDynamicScope(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) marked := testProject(t, "healthd-scope-on-") plain := testProject(t, "healthd-scope-off-") @@ -488,8 +482,8 @@ func TestE2EDynamicScope(t *testing.T) { // certificate cache, so a restricted daemon stays filtered on the old name. func TestE2EProjectRenamed(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-rename-") @@ -544,8 +538,8 @@ func TestE2EProjectRenamed(t *testing.T) { // not write must still end up corrected. func TestE2EStatusIsRepairedAfterAnotherWriter(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-restatus-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -574,8 +568,8 @@ func TestE2EStatusIsRepairedAfterAnotherWriter(t *testing.T) { // elapses, so the restart it queued must not fire. func TestE2ENoBounceAfterAnExternalStart(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-bounce-") name := testContainer(t, c, "web", healthKeys(map[string]string{ @@ -611,8 +605,8 @@ func TestE2ENoBounceAfterAnExternalStart(t *testing.T) { // stop` marks the instance, and unless-stopped must leave it alone. func TestE2ESchedulerHonoursAnIntentionalStop(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) c := testProject(t, "healthd-marked-") name := testContainer(t, c, "web", healthKeys(map[string]string{ diff --git a/cmd/ic-healthd/healthd_test.go b/cmd/ic-healthd/healthd_test.go index 1c32f08..0103210 100644 --- a/cmd/ic-healthd/healthd_test.go +++ b/cmd/ic-healthd/healthd_test.go @@ -20,13 +20,6 @@ import ( // checks use: true, false, wget and httpd. const testImage = "docker.io/library/busybox:glibc" -func skipLocal(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_LOCAL") - if ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_LOCAL is set, run `just test` for this test") - } -} - // testProject creates a throwaway Incus project, deleted on teardown. Each test // gets its own, which is also how the daemon is used. func testProject(t *testing.T, prefix string) *client.Client { diff --git a/cmd/ic-healthd/instance_actions_test.go b/cmd/ic-healthd/instance_actions_test.go index 73a7c84..857d38a 100644 --- a/cmd/ic-healthd/instance_actions_test.go +++ b/cmd/ic-healthd/instance_actions_test.go @@ -9,12 +9,13 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) // TestInstanceExecReportsTheExitCode pins the signal every check is built on. func TestInstanceExecReportsTheExitCode(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-exec-") name := testContainer(t, c, "web", nil, true) @@ -44,7 +45,7 @@ func TestInstanceExecReportsTheExitCode(t *testing.T) { // a failing check leaves to debug with. func TestInstanceExecCapturesOutput(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-exec-out-") name := testContainer(t, c, "web", nil, true) @@ -62,7 +63,7 @@ func TestInstanceExecCapturesOutput(t *testing.T) { // a command that never returns must not hold its instance for good. func TestInstanceExecHonoursTheContext(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-exec-ctx-") name := testContainer(t, c, "web", nil, true) @@ -88,7 +89,7 @@ func TestInstanceExecHonoursTheContext(t *testing.T) { // the health verdict the scheduler acts on. func TestInstanceCheckActionVerdicts(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-check-") name := testContainer(t, c, "web", nil, true) @@ -131,7 +132,7 @@ func TestInstanceCheckActionVerdicts(t *testing.T) { // a lifecycle fact, so the scheduler neither counts it nor writes a verdict. func TestInstanceCheckActionNotRunning(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-check-down-") name := testContainer(t, c, "web", nil, false) @@ -149,7 +150,7 @@ func TestInstanceCheckActionNotRunning(t *testing.T) { // not merely left alone. func TestInstanceRestartActionRestarts(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-restart-") name := testContainer(t, c, "web", nil, true) @@ -173,7 +174,7 @@ func TestInstanceRestartActionRestarts(t *testing.T) { // instance is already down, so there is nothing to stop first. func TestInstanceRestartActionStartsAStoppedInstance(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-restart-down-") name := testContainer(t, c, "web", nil, false) @@ -191,7 +192,7 @@ func TestInstanceRestartActionStartsAStoppedInstance(t *testing.T) { // never happen: undoing an `incus-compose stop`. func TestInstanceRestartActionRefusesAnIntentionalStop(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-restart-marked-") name := testContainer(t, c, "web", map[string]string{shared.HealthStoppedKey: "true"}, false) @@ -211,7 +212,7 @@ func TestInstanceRestartActionRefusesAnIntentionalStop(t *testing.T) { // patch, not a replace: it must not disturb keys it does not own. func TestPatchInstanceConfigWritesOnlyItsKeys(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-patch-") name := testContainer(t, c, "web", map[string]string{ @@ -235,7 +236,7 @@ func TestPatchInstanceConfigWritesOnlyItsKeys(t *testing.T) { // incus-compose wrote to what the scheduler runs on. func TestDiscoverInstanceReadsTheLiveKeys(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-discover-one-") name := testContainer(t, c, "web", healthKeys(map[string]string{ diff --git a/cmd/ic-healthd/main_test.go b/cmd/ic-healthd/main_test.go index 9df2519..6c69f5a 100644 --- a/cmd/ic-healthd/main_test.go +++ b/cmd/ic-healthd/main_test.go @@ -11,6 +11,7 @@ import ( "github.com/urfave/cli/v3" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) // runFlags parses args through the real run command and hands back the config @@ -268,7 +269,7 @@ func TestProjectSchedulerSurvivesADeadServer(t *testing.T) { // the only thing that finds instances that were already up. func TestDiscoverProjectSelectsWatchableInstances(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) c := testProject(t, "healthd-discover-") diff --git a/cmd/incus-compose/bind_mount_test.go b/cmd/incus-compose/bind_mount_test.go index 76dcb80..0bfd60f 100644 --- a/cmd/incus-compose/bind_mount_test.go +++ b/cmd/incus-compose/bind_mount_test.go @@ -10,11 +10,12 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/client" + "github.com/lxc/incus-compose/testlib" ) func TestBindMounts(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) pn := t.Name() compose := "../../test/fixtures/with-bind-mounts/compose.yaml" @@ -49,7 +50,7 @@ func TestBindMounts(t *testing.T) { func TestBindMountErrorsOnRemote(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) pn := t.Name() compose := "../../test/fixtures/with-bind-mounts/compose.yaml" @@ -70,7 +71,7 @@ func TestBindMountErrorsOnRemote(t *testing.T) { func TestSeededBindMounts(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) pn := t.Name() compose := "../../test/fixtures/with-seeded-bind-mounts/compose.yaml" @@ -102,7 +103,7 @@ func TestSeededBindMounts(t *testing.T) { func TestBindMountNoShift(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) pn := t.Name() compose := "../../test/fixtures/with-bind-mount-no-shift/compose.yaml" diff --git a/cmd/incus-compose/build_test.go b/cmd/incus-compose/build_test.go index fd9018f..1731320 100644 --- a/cmd/incus-compose/build_test.go +++ b/cmd/incus-compose/build_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/client" + "github.com/lxc/incus-compose/testlib" ) func skipIfNoBuilder(t *testing.T) { @@ -29,20 +30,9 @@ func skipIfNoBuilder(t *testing.T) { t.Skip("Skipping: podman or docker not found") } -func writeTempFiles(t *testing.T, files map[string]string) string { - t.Helper() - dir := t.TempDir() - for name, content := range files { - path := filepath.Join(dir, name) - require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700)) - require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) - } - return dir -} - func TestBuildCommandWithBuildFixture(t *testing.T) { - skipE2E(t) - skipLocal(t) + testlib.SkipE2E(t) + testlib.SkipLocal(t) skipIfNoBuilder(t) t.Parallel() @@ -69,14 +59,14 @@ func TestBuildCommandWithBuildFixture(t *testing.T) { } func TestBuildCommandWithServiceFilter(t *testing.T) { - skipE2E(t) - skipLocal(t) + testlib.SkipE2E(t) + testlib.SkipLocal(t) skipIfNoBuilder(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "Dockerfile": `FROM docker.io/alpine:latest AS runtime RUN echo "built by incus-compose" `, @@ -116,14 +106,14 @@ RUN echo "built by incus-compose" // TestE2EBuildImageEnvironment pins the built image to the environment.* keys // Incus derives itself when it unpacks a pulled OCI image. func TestE2EBuildImageEnvironment(t *testing.T) { - skipE2E(t) - skipLocal(t) + testlib.SkipE2E(t) + testlib.SkipLocal(t) skipIfNoBuilder(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "Dockerfile": `FROM docker.io/alpine:latest ENV GREETING=hello ENV PATH=/opt/bin:/usr/bin @@ -159,14 +149,14 @@ ENV PATH=/opt/bin:/usr/bin // TestE2EUpBuildRecreates pins that --build recreates the instances whose image // it rebuilt, and leaves every other service running as it was. func TestE2EUpBuildRecreates(t *testing.T) { - skipE2E(t) - skipLocal(t) + testlib.SkipE2E(t) + testlib.SkipLocal(t) skipIfNoBuilder(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "Dockerfile": `FROM docker.io/alpine:latest RUN echo "built by incus-compose" `, @@ -210,7 +200,7 @@ RUN echo "built by incus-compose" } func TestBuildCommandWithNoBuildServices(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -226,7 +216,7 @@ func TestBuildCommandWithNoBuildServices(t *testing.T) { } func TestBuildCommandWithNoMatchingBuildServices(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -242,12 +232,12 @@ func TestBuildCommandWithNoMatchingBuildServices(t *testing.T) { } func TestBuildCommandWithNonBuildServiceFilter(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: app: build: @@ -269,12 +259,12 @@ func TestBuildCommandWithNonBuildServiceFilter(t *testing.T) { } func TestBuildCommandRejectsMultiplePlatforms(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: app: build: @@ -298,12 +288,12 @@ func TestBuildCommandRejectsMultiplePlatforms(t *testing.T) { } func TestBuildCommandRejectsUnsupportedPlatform(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: app: build: @@ -326,11 +316,11 @@ func TestBuildCommandRejectsUnsupportedPlatform(t *testing.T) { } func TestBuildCommandReportsMissingBuilder(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: app: build: diff --git a/cmd/incus-compose/e2e_test.go b/cmd/incus-compose/e2e_test.go index bd39bd6..afef4d9 100644 --- a/cmd/incus-compose/e2e_test.go +++ b/cmd/incus-compose/e2e_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/client" + "github.com/lxc/incus-compose/testlib" ) func cleanLines(t *testing.T, in string) []string { @@ -25,8 +26,8 @@ func cleanLines(t *testing.T, in string) []string { // and does not wait on its (unstarted) service_healthy dependencies. func TestE2EUpNoDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -62,8 +63,8 @@ func TestE2EUpNoDeps(t *testing.T) { // marker content can only come from the pushed config. func TestE2EConfigOverwritesImageFile(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/with-configs/compose.yaml" @@ -90,8 +91,8 @@ func TestE2EConfigOverwritesImageFile(t *testing.T) { // replace from the append that `command:` alone still does. func TestE2EEntrypointReplacesImageEntrypoint(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -124,8 +125,8 @@ func TestE2EEntrypointReplacesImageEntrypoint(t *testing.T) { // linked services too. func TestE2EUpDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -157,8 +158,8 @@ func TestE2EUpDeps(t *testing.T) { // service and leaves its dependants running. func TestE2EDownNoDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -193,8 +194,8 @@ func TestE2EDownNoDeps(t *testing.T) { // and also removes the services that depend on the named one. func TestE2EDownDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -230,8 +231,8 @@ func TestE2EDownDeps(t *testing.T) { // (other running instances show up only as ). func TestE2EPsDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/proxy/compose.yaml" @@ -268,8 +269,8 @@ func TestE2EPsDeps(t *testing.T) { // dependency conditions); --with-deps follows depends_on like up/down. func TestE2EStartStopRestartWithDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/three-services/compose.yaml" @@ -318,8 +319,8 @@ func TestE2EStartStopRestartWithDeps(t *testing.T) { func TestE2EUpUp(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -345,8 +346,8 @@ func TestE2EUpUp(t *testing.T) { func TestE2EDownDown(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -376,8 +377,8 @@ func TestE2EDownDown(t *testing.T) { func TestE2EDownProjectDeletesNetworks(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -419,8 +420,8 @@ func TestE2EDownProjectDeletesNetworks(t *testing.T) { func TestE2EUpRecreate(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -446,8 +447,8 @@ func TestE2EUpRecreate(t *testing.T) { func TestE2EUpUpRecreate(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -481,8 +482,8 @@ func TestE2EUpUpRecreate(t *testing.T) { func TestE2EUpRecreateDown(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -516,8 +517,8 @@ func TestE2EUpRecreateDown(t *testing.T) { func TestE2ELifecycleSimple(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -580,8 +581,8 @@ func TestE2ELifecycleSimple(t *testing.T) { func TestE2EUpDownScale(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -607,8 +608,8 @@ func TestE2EUpDownScale(t *testing.T) { func TestE2EUpDownDownscale(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -634,8 +635,8 @@ func TestE2EUpDownDownscale(t *testing.T) { func TestE2EUpDownWithScale(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -661,8 +662,8 @@ func TestE2EUpDownWithScale(t *testing.T) { func TestE2EListSnapshots(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -691,8 +692,8 @@ func TestE2EListSnapshots(t *testing.T) { func TestE2EExternalNetwork(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -729,8 +730,8 @@ func TestE2EExternalNetwork(t *testing.T) { func TestE2EUpDownWithIncusOptions(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -756,8 +757,8 @@ func TestE2EUpDownWithIncusOptions(t *testing.T) { func TestE2EUpDownWithProjectOptions(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -783,8 +784,8 @@ func TestE2EUpDownWithProjectOptions(t *testing.T) { func TestE2EUpDownWithSecrets(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -810,8 +811,8 @@ func TestE2EUpDownWithSecrets(t *testing.T) { func TestE2EUpDownWithSecretsVerifyFiles(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -851,8 +852,8 @@ func TestE2EUpDownWithSecretsVerifyFiles(t *testing.T) { func TestE2EUpDownWithConfigs(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -878,8 +879,8 @@ func TestE2EUpDownWithConfigs(t *testing.T) { func TestE2EUpDownWithConfigsVerifyFiles(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -919,8 +920,8 @@ func TestE2EUpDownWithConfigsVerifyFiles(t *testing.T) { func TestE2EDownImages(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -954,8 +955,8 @@ func TestE2EDownImages(t *testing.T) { func TestE2EUpDownWithVolume(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -984,8 +985,8 @@ func TestE2EUpDownWithVolume(t *testing.T) { // applies only to that invocation; the next plain `up` restores replicas. func TestE2EUpReconcilesToReplicas(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() diff --git a/cmd/incus-compose/exec_test.go b/cmd/incus-compose/exec_test.go index 8fd8f3b..09c7c83 100644 --- a/cmd/incus-compose/exec_test.go +++ b/cmd/incus-compose/exec_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // TestExecSelectsCorrectInstance is a regression test for the exec command @@ -15,8 +17,8 @@ import ( // the output matches the expected Incus instance name. func TestExecSelectsCorrectInstance(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -54,8 +56,8 @@ func TestExecSelectsCorrectInstance(t *testing.T) { // to the id-shifted named volume succeeds and the file lands owned by 1000:1000. func TestE2EExecRunsAsInstanceUser(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() diff --git a/cmd/incus-compose/healthd_e2e_test.go b/cmd/incus-compose/healthd_e2e_test.go index 92bcedb..13d0da6 100644 --- a/cmd/incus-compose/healthd_e2e_test.go +++ b/cmd/incus-compose/healthd_e2e_test.go @@ -13,6 +13,7 @@ import ( "github.com/lxc/incus-compose/client" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) const healthdScopeCompose = "../../test/fixtures/healthd-scope/compose.yaml" @@ -51,8 +52,8 @@ func waitHealthy(t *testing.T, c *client.Client, name string) { // own, one shared daemon in its own project, and the project marked so the // daemon picks it up. func TestE2EHealthdGlobalScope(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -96,13 +97,13 @@ func TestE2EHealthdGlobalScope(t *testing.T) { // // Not parallel: it recreates the daemon every other project uses. func TestE2EHealthdGlobalComposeNetwork(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := strings.ToLower(t.Name()) - dir := writeTempFiles(t, map[string]string{"compose.yaml": `x-incus-compose: + dir := testlib.WriteTempFiles(t, map[string]string{"compose.yaml": `x-incus-compose: healthd: network: ` + pn + `:hnet networks: @@ -154,8 +155,8 @@ services: // TestE2EHealthdProjectScope keeps the old topology when asked for it. func TestE2EHealthdProjectScope(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -181,8 +182,8 @@ func TestE2EHealthdProjectScope(t *testing.T) { // TestE2EHealthdCoexistence is the load-bearing case: a project-scoped daemon // and the shared one must both work and neither may watch the other's project. func TestE2EHealthdCoexistence(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() globalPN := t.Name() + "-global" @@ -245,8 +246,8 @@ func TestE2EHealthdCoexistence(t *testing.T) { // // Not parallel: it creates and removes the daemon every other project uses. func TestE2EHealthdNoComposeFile(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() dir := t.TempDir() @@ -295,8 +296,8 @@ func TestE2EHealthdNoComposeFile(t *testing.T) { // // Not parallel: it removes the daemon every other project uses. func TestE2EHealthdDownNeedsForce(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() one := t.Name() + "-one" @@ -335,8 +336,8 @@ func TestE2EHealthdDownNeedsForce(t *testing.T) { // TestE2EHealthdMigratesToGlobal covers the upgrade path: the project sidecar is // removed before the project is marked, so the two never overlap. func TestE2EHealthdMigratesToGlobal(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() diff --git a/cmd/incus-compose/healthd_test.go b/cmd/incus-compose/healthd_test.go index c20c8cb..899379a 100644 --- a/cmd/incus-compose/healthd_test.go +++ b/cmd/incus-compose/healthd_test.go @@ -10,6 +10,7 @@ import ( "github.com/lxc/incus-compose/client" "github.com/lxc/incus-compose/project" + "github.com/lxc/incus-compose/testlib" ) func TestParseHealthdNetwork(t *testing.T) { @@ -90,8 +91,8 @@ func TestParseHealthdNetwork(t *testing.T) { // This test is very buggy and the root of a lot of pain for me. // func TestLifecycleHealthd(t *testing.T) { // t.Parallel() -// skipLocal(t) -// skipE2E(t) +// testlib.SkipLocal(t) +// testlib.SkipE2E(t) // ctx := context.Background() // pn := t.Name() @@ -142,8 +143,8 @@ func TestParseHealthdNetwork(t *testing.T) { // } func TestNoHealthdSkipsHealthdInstance(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() @@ -174,8 +175,8 @@ func TestNoHealthdSkipsHealthdInstance(t *testing.T) { } func TestNoHealthdWhenNotNeeded(t *testing.T) { - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() diff --git a/cmd/incus-compose/main_test.go b/cmd/incus-compose/main_test.go index 1bc4005..39a03db 100644 --- a/cmd/incus-compose/main_test.go +++ b/cmd/incus-compose/main_test.go @@ -5,7 +5,6 @@ import ( "context" "os" "path/filepath" - "regexp" "strings" "testing" "time" @@ -16,24 +15,11 @@ import ( "github.com/lxc/incus-compose/client" "github.com/lxc/incus-compose/project" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) var snapshotter = cupaloy.New(cupaloy.SnapshotSubdirectory(filepath.Join("..", "..", "test", "snapshots", "e2e"))) -func skipLocal(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_LOCAL") - if ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_LOCAL is set, run `just test` for this test") - } -} - -func skipE2E(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_E2E") - if !ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_E2E is not set, run `just test-e2e` for this test") - } -} - func skipNo73(t *testing.T, c *client.Client) { if !c.Global().HasExtension(shared.Incus73Extension) { t.Skip("nat tests with static ip require at least incus 7.3 or 7.0.2 LTS") @@ -49,10 +35,7 @@ func skipNotSameHost(t *testing.T, gc *client.GlobalClient) { func runCommand(ctx context.Context, t *testing.T, projectName string, args ...string) (*bytes.Buffer, error) { t.Helper() - projectName = strings.ToLower(strings.ReplaceAll(projectName, "/", "-")) - - mArgs := []string{"incus-compose", "--debug", "--project-name", projectName} - mArgs = append(mArgs, args...) + mArgs := append([]string{"incus-compose"}, testlib.Args(projectName, args...)...) t.Log("Running", mArgs) stdout := &bytes.Buffer{} @@ -86,15 +69,15 @@ func runCommandSnapshotList(ctx context.Context, t *testing.T, projectName strin // This makes sure that health status settles and makes tests less flaky. time.Sleep(500 * time.Millisecond) - projectName = strings.ToLower(strings.ReplaceAll(projectName, "/", "-")) - listArgs := []string{"incus-compose", "--debug", "--project-name", projectName} + forwarded := []string{} for i, a := range args { if (a == "-f" || a == "--file") && i+1 < len(args) { - listArgs = append(listArgs, a, args[i+1]) + forwarded = append(forwarded, a, args[i+1]) } } - listArgs = append(listArgs, "list", "--format=json") + forwarded = append(forwarded, "list", "--format=json") + listArgs := append([]string{"incus-compose"}, testlib.Args(projectName, forwarded...)...) t.Log("Running", listArgs) @@ -108,35 +91,23 @@ func runCommandSnapshotList(ctx context.Context, t *testing.T, projectName strin snapshotter.SnapshotT(t, stripOutput(t, stdout, strip)) } -// stripOutput removes dynamic content (IP addresses, network hashes) for snapshot comparison. func stripOutput(t *testing.T, output *bytes.Buffer, stripHealth bool) string { t.Helper() - ipv4Regex := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`) - ipv6Regex := regexp.MustCompile(`(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}`) - healthdImageRegex := regexp.MustCompile("ic-healthd:[0-9a-z.-]+") - outStr := ipv4Regex.ReplaceAllString(output.String(), "-stripped-") - outStr = ipv6Regex.ReplaceAllString(outStr, "-stripped-") - outStr = healthdImageRegex.ReplaceAllString(outStr, "ic-healthd:-stripped-") - if stripHealth { - healthRegex := regexp.MustCompile(`"health": "[a-zA-Z]+",`) - outStr = healthRegex.ReplaceAllString(outStr, `"health": "-stripped-",`) + return testlib.StripHealth(output.String()) } - // Cupaloy adds a newline, 2 lines are bad for my editors format on save. - return strings.Trim(outStr, "\n") + return testlib.Strip(output.String()) } func plannedNetworkNames(ctx context.Context, t *testing.T, projectName, compose string) []string { t.Helper() - projectName = strings.ToLower(strings.ReplaceAll(projectName, "/", "-")) - p, err := project.New().Load(ctx, project.LoadFiles([]string{compose})) require.NoError(t, err) - c := client.NewOfflineClient(ctx, projectName) + c := client.NewOfflineClient(ctx, testlib.ProjectName(projectName)) allResources, err := p.Resources(c) require.NoError(t, err) @@ -384,7 +355,7 @@ func TestConfigFilterByService(t *testing.T) { } func TestUpDownUpSimple(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -420,7 +391,7 @@ func TestUpDownUpSimple(t *testing.T) { } func TestNormalLifecycle(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -465,7 +436,7 @@ func dnsServiceIPs(t *testing.T, c *client.Client, networks []string, service st // downscales to a single instance with --scale and verifies both the surplus // instances and their DNS records are removed while the survivor keeps resolving. func TestUpDownscaleRemovesInstancesAndDNS(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -517,7 +488,7 @@ func TestUpDownscaleRemovesInstancesAndDNS(t *testing.T) { // Snapshotting dns's network raw.dnsmasq confirms both projects' cnames // coexist without clobbering each other. func TestDNSCnameAliasAcrossProjects(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -550,19 +521,5 @@ func TestDNSCnameAliasAcrossProjects(t *testing.T) { net, _, err := conn.GetNetwork(ctx, networkName) require.NoError(t, err) - ipv4Regex := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`) - ipv6Regex := regexp.MustCompile(`(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}`) - - lines := strings.Split(net.Config["raw.dnsmasq"], "\n") - kept := make([]string, 0, len(lines)) - for _, line := range lines { - if ipv6Regex.MatchString(line) { - continue - } - kept = append(kept, line) - } - - outStr := ipv4Regex.ReplaceAllString(strings.Join(kept, "\n"), "-stripped-") - - snapshotter.SnapshotT(t, outStr) + snapshotter.SnapshotT(t, testlib.Strip(testlib.StripIPv6Lines(net.Config["raw.dnsmasq"]))) } diff --git a/cmd/incus-compose/nat_proxy_test.go b/cmd/incus-compose/nat_proxy_test.go index 28dd20d..ed23cd8 100644 --- a/cmd/incus-compose/nat_proxy_test.go +++ b/cmd/incus-compose/nat_proxy_test.go @@ -9,14 +9,15 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/client" + "github.com/lxc/incus-compose/testlib" ) // TestE2ENatProxy verifies that published ports create NAT proxy devices // with the correct configuration (nat=true, wildcard connect address). func TestE2ENATProxy(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -53,13 +54,13 @@ func TestE2ENATProxy(t *testing.T) { func TestE2ENATProxyWithPort(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: web: image: docker.io/nginx:alpine @@ -94,8 +95,8 @@ func TestE2ENATProxyWithPort(t *testing.T) { func TestE2ENATProxyWithPortAndStaticIP(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -106,7 +107,7 @@ func TestE2ENATProxyWithPortAndStaticIP(t *testing.T) { skipNo73(t, c) - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: web: image: docker.io/nginx:alpine diff --git a/cmd/incus-compose/pull_test.go b/cmd/incus-compose/pull_test.go index 6992ff1..69f311a 100644 --- a/cmd/incus-compose/pull_test.go +++ b/cmd/incus-compose/pull_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // pulledImageAliases runs the wrapped `incus image list --format=json` inside the @@ -47,8 +49,8 @@ func hasImage(aliases []string, sub string) bool { // verified through the wrapped `incus image list --format=json`. func TestE2EPull(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -92,8 +94,8 @@ func TestE2EPull(t *testing.T) { // service's image while `pull --with-deps ` also follows depends_on. func TestE2EPullWithDeps(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() @@ -126,12 +128,12 @@ func TestE2EPullWithDeps(t *testing.T) { // image that cannot be resolved from any registry. func TestE2EPullInvalidImage(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: bogus: image: docker.io/library/incus-compose-does-not-exist:latest @@ -151,8 +153,8 @@ func TestE2EPullInvalidImage(t *testing.T) { // build config; plain pull tries (and fails) to pull them from a registry. func TestE2EPullIgnoreBuildable(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) ctx := t.Context() pn := t.Name() diff --git a/cmd/incus-compose/regression_test.go b/cmd/incus-compose/regression_test.go index 12560f6..5ed7049 100644 --- a/cmd/incus-compose/regression_test.go +++ b/cmd/incus-compose/regression_test.go @@ -7,13 +7,14 @@ import ( "github.com/stretchr/testify/require" "github.com/lxc/incus-compose/client" + "github.com/lxc/incus-compose/testlib" ) // TestNoDanglingNetworksAfterDown is a regression test for the project default // network not being removed after `down --project`. func TestNoDanglingNetworksAfterDown(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() pn := t.Name() @@ -45,8 +46,8 @@ func TestNoDanglingNetworksAfterDown(t *testing.T) { // TestE2EStartStopIdempotent checks that running start/stop twice (idempotent) works without errors. func TestE2EStartStopIdempotent(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/simple/compose.yaml" @@ -88,8 +89,8 @@ func TestE2EStartStopIdempotent(t *testing.T) { func TestE2ENoImageCache(t *testing.T) { t.Parallel() - skipLocal(t) - skipE2E(t) + testlib.SkipLocal(t) + testlib.SkipE2E(t) compose := "../../test/fixtures/simple/compose.yaml" diff --git a/cmd/incus-compose/up_test.go b/cmd/incus-compose/up_test.go index 302edf3..3ce11f0 100644 --- a/cmd/incus-compose/up_test.go +++ b/cmd/incus-compose/up_test.go @@ -11,6 +11,7 @@ import ( "github.com/lxc/incus-compose/cmd/incus-compose/version" "github.com/lxc/incus-compose/project" + "github.com/lxc/incus-compose/testlib" ) func TestVersionCommand(t *testing.T) { @@ -40,7 +41,7 @@ func TestResolveHealthdImage(t *testing.T) { func TestBuiltServices(t *testing.T) { t.Parallel() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "Dockerfile": "FROM docker.io/alpine:latest\n", "compose.yaml": `name: built services: diff --git a/cmd/incus-compose/wellknown_test.go b/cmd/incus-compose/wellknown_test.go index d764f2f..d57f9e1 100644 --- a/cmd/incus-compose/wellknown_test.go +++ b/cmd/incus-compose/wellknown_test.go @@ -6,16 +6,18 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func TestWellKnownRegistryQuayIO(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: hello: image: quay.io/podman/hello @@ -33,12 +35,12 @@ func TestWellKnownRegistryQuayIO(t *testing.T) { func TestWellKnownRegistryMCR(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) ctx := t.Context() pn := t.Name() - dir := writeTempFiles(t, map[string]string{ + dir := testlib.WriteTempFiles(t, map[string]string{ "compose.yaml": `services: hello: image: mcr.microsoft.com/azurelinux/busybox:1.36 diff --git a/examples/examples_test.go b/examples/examples_test.go index 31caf3b..bce71df 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -4,34 +4,24 @@ import ( "bytes" "context" "log/slog" - "os" "os/exec" "path/filepath" - "regexp" - "strings" "testing" "time" "github.com/bradleyjkemp/cupaloy/v2" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) var snapshotter = cupaloy.New(cupaloy.SnapshotSubdirectory(filepath.Join("..", "test", "snapshots", "examples"))) -func skipExamples(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_EXAMPLES") - if !ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_EXAMPLES is not set, run `just test-examples` for this test") - } -} - func runCommand(ctx context.Context, t *testing.T, projectName string, args ...string) (*bytes.Buffer, error) { t.Helper() - projectName = strings.ToLower(strings.ReplaceAll(projectName, "/", "-")) - - mArgs := []string{"run", "--", "github.com/lxc/incus-compose/cmd/incus-compose/...", "--debug", "--project-name", projectName} - mArgs = append(mArgs, args...) + mArgs := append([]string{"run", "--", "github.com/lxc/incus-compose/cmd/incus-compose/..."}, + testlib.Args(projectName, args...)...) slog.DebugContext(ctx, "Running", "args", mArgs) stdout := &bytes.Buffer{} @@ -43,19 +33,10 @@ func runCommand(ctx context.Context, t *testing.T, projectName string, args ...s return stdout, err } -// stripOutput removes dynamic content (IP addresses, network hashes) for snapshot comparison. func stripOutput(t *testing.T, output *bytes.Buffer) string { t.Helper() - ipv4Regex := regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`) - ipv6Regex := regexp.MustCompile(`(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}`) - healthdImageRegex := regexp.MustCompile("ic-healthd:[0-9a-z.-]+") - outStr := ipv4Regex.ReplaceAllString(output.String(), "-stripped-") - outStr = ipv6Regex.ReplaceAllString(outStr, "-stripped-") - outStr = healthdImageRegex.ReplaceAllString(outStr, "ic-healthd:-stripped-") - - // Cupaloy adds a newline, 2 lines are bad for my editors format on save. - return strings.Trim(outStr, "\n") + return testlib.Strip(output.String()) } // func TestMain(m *testing.M) { @@ -72,7 +53,7 @@ func stripOutput(t *testing.T, output *bytes.Buffer) string { func TestExample(t *testing.T) { t.Parallel() - skipExamples(t) + testlib.SkipExamples(t) examples := []struct { name string diff --git a/iclient/incus_busy_test.go b/iclient/incus_busy_test.go index f829419..e7a95e1 100644 --- a/iclient/incus_busy_test.go +++ b/iclient/incus_busy_test.go @@ -11,6 +11,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // busyMessage is what incusd sends, verbatim from operationlock. The quotes @@ -132,7 +134,7 @@ func TestIncusWaitInstanceBusyReturnsWhenNothingHoldsIt(t *testing.T) { // TestE2EWaitInstanceBusyWaitsOutAnOperation is the contract against a real // server: it returns only once the operation holding the instance is done. func TestE2EWaitInstanceBusyWaitsOutAnOperation(t *testing.T) { - skipE2E(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_console_test.go b/iclient/incus_console_test.go index b95449c..769db34 100644 --- a/iclient/incus_console_test.go +++ b/iclient/incus_console_test.go @@ -12,6 +12,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // TestIncusConsoleInstanceRefusesWithoutOutput: there is nowhere to put the @@ -98,7 +100,7 @@ func TestIncusGetInstanceConsoleLogURL(t *testing.T) { // TestIncusConsoleInstanceAgainstRealIncus reads a running instance's console, // which is what `incus-compose logs` is built on. func TestIncusConsoleInstanceAgainstRealIncus(t *testing.T) { - skipE2E(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_events_test.go b/iclient/incus_events_test.go index bb64793..a5dfb5f 100644 --- a/iclient/incus_events_test.go +++ b/iclient/incus_events_test.go @@ -11,6 +11,8 @@ import ( "github.com/gorilla/websocket" "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // eventServer serves /1.0/events over a websocket, writing each event it is @@ -350,7 +352,7 @@ func TestIncusListenEventsScopedEndsOnItsOwnContext(t *testing.T) { } func TestIncusListenEventsAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() conn := testConnection(t) diff --git a/iclient/incus_exec_test.go b/iclient/incus_exec_test.go index 73fe51b..fe8d505 100644 --- a/iclient/incus_exec_test.go +++ b/iclient/incus_exec_test.go @@ -8,6 +8,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // TestIncusExecInstanceRefusesInteractive: a PTY needs control and resize @@ -95,7 +97,7 @@ func testInstance(t *testing.T, conn *Connection, name string, config map[string // TestIncusExecInstanceAgainstRealIncus runs a command and reads its output // and exit code, which is the whole of what healthd needs. func TestIncusExecInstanceAgainstRealIncus(t *testing.T) { - skipE2E(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_image_test.go b/iclient/incus_image_test.go index 4fe9357..fe7a89c 100644 --- a/iclient/incus_image_test.go +++ b/iclient/incus_image_test.go @@ -16,6 +16,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // waitOperation drains an operation channel and returns its outcome, which is @@ -61,7 +63,7 @@ func testProject(t *testing.T, conn *Connection, prefix string) string { // OCI image from a registry into one project, then copy it into another. // Neither hop touches a registry from here; incusd does the fetching. func TestIncusImagePullAndCopy(t *testing.T) { - skipE2E(t) + testlib.SkipE2E(t) t.Parallel() ctx := t.Context() @@ -272,7 +274,7 @@ func TestIncusCreateImageRefusesWithoutMetadata(t *testing.T) { // TestIncusCreateImageUploadAgainstRealIncus imports a split image the way the // compose `build:` path does, from tarballs rather than from a remote. func TestIncusCreateImageUploadAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_network_test.go b/iclient/incus_network_test.go index 7775f8c..5b4b8db 100644 --- a/iclient/incus_network_test.go +++ b/iclient/incus_network_test.go @@ -6,6 +6,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func TestIncusNetworkRequests(t *testing.T) { @@ -104,7 +106,7 @@ func TestIncusNetworkRequests(t *testing.T) { } func TestIncusNetworkAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_operation_test.go b/iclient/incus_operation_test.go index bb50aab..2f44f88 100644 --- a/iclient/incus_operation_test.go +++ b/iclient/incus_operation_test.go @@ -12,6 +12,8 @@ import ( "github.com/gorilla/websocket" "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // operationServer answers /1.0/events over a websocket and every other path @@ -257,7 +259,7 @@ func TestIncusGetOperationsFlattens(t *testing.T) { } func TestIncusOperationsAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() conn := testConnection(t) diff --git a/iclient/incus_profile_test.go b/iclient/incus_profile_test.go index b6740f5..975fd09 100644 --- a/iclient/incus_profile_test.go +++ b/iclient/incus_profile_test.go @@ -6,6 +6,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func TestIncusProfileRequests(t *testing.T) { @@ -68,7 +70,7 @@ func TestIncusProfileRequests(t *testing.T) { } func TestIncusProfileAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_server_test.go b/iclient/incus_server_test.go index 4353e05..5494db2 100644 --- a/iclient/incus_server_test.go +++ b/iclient/incus_server_test.go @@ -9,6 +9,8 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) func TestIncusServerRequestURLs(t *testing.T) { @@ -92,7 +94,7 @@ func TestIncusGetConnectionInfoDefaultProject(t *testing.T) { } func TestIncusServerAgainstRealIncus(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() diff --git a/iclient/incus_sftp_test.go b/iclient/incus_sftp_test.go index ad7c26a..b266ad7 100644 --- a/iclient/incus_sftp_test.go +++ b/iclient/incus_sftp_test.go @@ -7,12 +7,14 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" + + "github.com/lxc/incus-compose/testlib" ) // TestIncusVolumeSFTP writes a file into a custom volume and reads it back, // which is the path volume seeding and the image lock both take. func TestIncusVolumeSFTP(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -70,7 +72,7 @@ func TestIncusVolumeSFTP(t *testing.T) { // TestIncusInstanceSFTPNotFound pins the refused-upgrade path: the server's // own error comes back, mapped like every other call's. func TestIncusInstanceSFTPNotFound(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() conn := testConnection(t) diff --git a/iclient/incus_test.go b/iclient/incus_test.go index 4355734..ac0e09d 100644 --- a/iclient/incus_test.go +++ b/iclient/incus_test.go @@ -14,16 +14,9 @@ import ( "github.com/lxc/incus/v7/shared/api" "github.com/stretchr/testify/require" -) - -// skipLocal skips a test that needs a real Incus server. -func skipLocal(t *testing.T) { - t.Helper() - if os.Getenv("INCUS_COMPOSE_TEST_LOCAL") != "" { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_LOCAL is set, run `just test` for this test") - } -} + "github.com/lxc/incus-compose/testlib" +) // testConnection dials the remote the test environment points at. func testConnection(t *testing.T) *Connection { @@ -213,7 +206,7 @@ func TestIncusSocketPathFromDir(t *testing.T) { } func TestIncusGetInstanceNames(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() conn := testConnection(t) @@ -228,7 +221,7 @@ func TestIncusGetInstanceNames(t *testing.T) { } func TestIncusGetInstancesRecursion(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -251,7 +244,7 @@ func TestIncusGetInstancesRecursion(t *testing.T) { // TestIncusGetInstanceNotFound pins the error mapping: an API error envelope // has to come back as a 404 StatusError, not as a decode failure or a nil. func TestIncusGetInstanceNotFound(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -269,7 +262,7 @@ func TestIncusGetInstanceNotFound(t *testing.T) { // TestIncusGetInstanceRoundTrip only runs where the remote already has an // instance; it checks the single-instance calls agree with the list ones. func TestIncusGetInstanceRoundTrip(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() ctx := t.Context() @@ -301,7 +294,7 @@ func TestIncusGetInstanceRoundTrip(t *testing.T) { // TestIncusUnknownProjectIsEmpty pins what the server actually does: an // unknown project is an empty collection, not a 404. func TestIncusUnknownProjectIsEmpty(t *testing.T) { - skipLocal(t) + testlib.SkipLocal(t) t.Parallel() config, err := ReadConfig("") @@ -648,12 +641,3 @@ func TestIncusContextCancelled(t *testing.T) { _, err = conn.GetInstanceNames(ctx, nil) require.ErrorIs(t, err, context.Canceled) } - -// skipE2E skips a slow test that drives a real registry. -func skipE2E(t *testing.T) { - t.Helper() - - if os.Getenv("INCUS_COMPOSE_TEST_E2E") == "" { - t.Skip("Skipping: set INCUS_COMPOSE_TEST_E2E=1, or run `just test-e2e`") - } -} diff --git a/project/instance_test.go b/project/instance_test.go index 59bdbed..b8a02a8 100644 --- a/project/instance_test.go +++ b/project/instance_test.go @@ -11,6 +11,7 @@ import ( "github.com/lxc/incus-compose/client" "github.com/lxc/incus-compose/shared" + "github.com/lxc/incus-compose/testlib" ) func TestFormatMemoryLimit(t *testing.T) { @@ -162,7 +163,7 @@ func TestInstanceName(t *testing.T) { func TestInstanceConfig(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) gc, err := client.NewTestClient(t.Context()) require.NoError(t, err) @@ -415,7 +416,7 @@ func TestInstanceConfigSysctls(t *testing.T) { func TestInstanceConfigMinimal(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) gc, err := client.NewTestClient(t.Context()) require.NoError(t, err) @@ -435,7 +436,7 @@ func TestInstanceConfigMinimal(t *testing.T) { func TestInstanceConfigXIncusOverrides(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) gc, err := client.NewTestClient(t.Context()) require.NoError(t, err) @@ -863,7 +864,7 @@ func TestInstanceImage(t *testing.T) { func TestInstanceNetworkDevices(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) gc, err := client.NewTestClient(t.Context()) require.NoError(t, err) @@ -962,7 +963,7 @@ func TestInstanceNetworkDevices(t *testing.T) { func TestInstanceProxyDevices(t *testing.T) { t.Parallel() - skipLocal(t) + testlib.SkipLocal(t) gc, err := client.NewTestClient(t.Context()) require.NoError(t, err) diff --git a/project/project_test.go b/project/project_test.go index 54621fd..796b945 100644 --- a/project/project_test.go +++ b/project/project_test.go @@ -18,13 +18,6 @@ func fixturePath(name string) string { return filepath.Join("..", "test", "fixtures", name) } -func skipLocal(t *testing.T) { - _, ok := os.LookupEnv("INCUS_COMPOSE_TEST_LOCAL") - if ok { - t.Skip("Skipping: env INCUS_COMPOSE_TEST_LOCAL is set, run `just test` for this test") - } -} - func skipNo73(t *testing.T, c *client.Client) { if !c.Global().HasExtension(shared.Incus73Extension) { t.Skip("nat tests with static ip require at least incus 7.3 or 7.0.2 LTS") diff --git a/testlib/compose.go b/testlib/compose.go new file mode 100644 index 0000000..69e38ad --- /dev/null +++ b/testlib/compose.go @@ -0,0 +1,15 @@ +package testlib + +import "strings" + +// ProjectName makes an Incus project name out of anything, so a test can pass +// t.Name() and get one back. +func ProjectName(name string) string { + return strings.ToLower(strings.ReplaceAll(name, "/", "-")) +} + +// Args builds the incus-compose arguments for a project, ahead of whatever the +// caller runs them with. +func Args(project string, args ...string) []string { + return append([]string{"--debug", "--project-name", ProjectName(project)}, args...) +} diff --git a/testlib/doc.go b/testlib/doc.go new file mode 100644 index 0000000..257fe00 --- /dev/null +++ b/testlib/doc.go @@ -0,0 +1,9 @@ +// Package testlib holds what every package's tests need: the tier guards, the +// naming and argument conventions, and snapshot normalization. +// +// It may import the standard library and external modules, and nothing from +// incus-compose. client, iclient and project test in-package, so a helper here +// that reached for one of them would be an import cycle for exactly the tests +// that need it most. A helper that does need our own types belongs in the +// package it serves. +package testlib diff --git a/testlib/files.go b/testlib/files.go new file mode 100644 index 0000000..e9a4706 --- /dev/null +++ b/testlib/files.go @@ -0,0 +1,25 @@ +package testlib + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// WriteTempFiles writes files, keyed by a path relative to a fresh temp +// directory, and returns that directory. Parents are created as needed, so a +// key may nest. +func WriteTempFiles(t *testing.T, files map[string]string) string { + t.Helper() + + dir := t.TempDir() + for name, content := range files { + path := filepath.Join(dir, name) + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700)) + require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) + } + + return dir +} diff --git a/testlib/incus.go b/testlib/incus.go new file mode 100644 index 0000000..8dcc8ae --- /dev/null +++ b/testlib/incus.go @@ -0,0 +1,128 @@ +// Incus API values for tests that must not talk to a daemon. A value built here +// encodes a guess at what incusd returns, so a test that turns on the daemon's +// real behavior proves nothing against it and belongs in a tier that has one. + +package testlib + +import ( + "fmt" + + incusapi "github.com/lxc/incus/v7/shared/api" +) + +// LabelPrefix is the namespace an instance or a project configures us from. +const LabelPrefix = "user.label.coredns." + +// Project is one project's worth of Incus values, as three reads would return them. +type Project struct { + Project incusapi.Project + Networks []incusapi.Network + Instances []incusapi.Instance + + // States is what GetInstanceState answers, keyed by instance name. + States map[string]*incusapi.InstanceState +} + +// NewProject builds a project with the given number of instances and networks. +// Everything derives from the index: net owns 10..0.0/24, and inst sits +// on network i%networks holding 10..0.<10+i>. +func NewProject(name string, instances, networks int) *Project { + p := &Project{ + Project: incusapi.Project{ + Name: name, + ProjectPut: incusapi.ProjectPut{Config: map[string]string{}}, + }, + States: map[string]*incusapi.InstanceState{}, + } + + for n := range networks { + p.Networks = append(p.Networks, NewNetwork(name, n)) + } + + for i := range instances { + on := 0 + if networks > 0 { + on = i % networks + } + + inst := NewInstance(name, i, on) + p.Instances = append(p.Instances, inst) + p.States[inst.Name] = NewInstanceState(i, on) + } + + return p +} + +// NewNetwork builds the nth managed bridge of a project. +func NewNetwork(project string, n int) incusapi.Network { + return incusapi.Network{ + Name: NetworkName(n), + Project: project, + Managed: true, + Type: "bridge", + NetworkPut: incusapi.NetworkPut{ + Config: map[string]string{ + "ipv4.address": fmt.Sprintf("10.%d.0.1/24", n), + "ipv6.address": "none", + }, + }, + } +} + +// NewInstance builds the ith running instance of a project, on network on. Its +// NIC is an expanded device, as a profile-supplied one is, so Devices is empty. +func NewInstance(project string, i, on int) incusapi.Instance { + name := InstanceName(i) + + nic := map[string]string{"type": "nic", "network": NetworkName(on)} + + return incusapi.Instance{ + Name: name, + Project: project, + Status: "Running", + StatusCode: incusapi.Running, + InstancePut: incusapi.InstancePut{ + Config: map[string]string{}, + Devices: map[string]map[string]string{}, + }, + ExpandedConfig: map[string]string{}, + ExpandedDevices: map[string]map[string]string{"eth0": nic}, + } +} + +// NewInstanceState builds the state of the ith instance on network on: one +// global address on eth0, plus loopback. +func NewInstanceState(i, on int) *incusapi.InstanceState { + return &incusapi.InstanceState{ + Status: "Running", + StatusCode: incusapi.Running, + Network: map[string]incusapi.InstanceStateNetwork{ + "lo": { + Type: "loopback", + Addresses: []incusapi.InstanceStateNetworkAddress{ + {Family: "inet", Address: "127.0.0.1", Scope: "local"}, + }, + }, + "eth0": { + Type: "broadcast", + Addresses: []incusapi.InstanceStateNetworkAddress{ + {Family: "inet", Address: Address(on, i), Netmask: "24", Scope: "global"}, + }, + }, + }, + } +} + +// InstanceName is what the ith instance is called. +func InstanceName(i int) string { return fmt.Sprintf("inst%d", i) } + +// NetworkName is what the nth network is called. +func NetworkName(n int) string { return fmt.Sprintf("net%d", n) } + +// Address is the address the ith instance holds on the nth network. +func Address(n, i int) string { return fmt.Sprintf("10.%d.0.%d", n, 10+i) } + +// Label writes one of our keys, prefix and all, onto a config map. +func Label(config map[string]string, key, value string) { + config[LabelPrefix+key] = value +} diff --git a/testlib/snapshot.go b/testlib/snapshot.go new file mode 100644 index 0000000..24d2641 --- /dev/null +++ b/testlib/snapshot.go @@ -0,0 +1,42 @@ +package testlib + +import ( + "regexp" + "strings" +) + +var ( + ipv4Regex = regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`) + ipv6Regex = regexp.MustCompile(`(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}`) + healthdImageRegex = regexp.MustCompile("ic-healthd:[0-9a-z.-]+") + healthRegex = regexp.MustCompile(`"health": "[a-zA-Z]+",`) +) + +// Strip replaces what changes between runs - addresses and the healthd image +// tag - so output can be snapshotted. The trailing newline goes too, since +// cupaloy adds one back. +func Strip(out string) string { + out = ipv4Regex.ReplaceAllString(out, "-stripped-") + out = ipv6Regex.ReplaceAllString(out, "-stripped-") + out = healthdImageRegex.ReplaceAllString(out, "ic-healthd:-stripped-") + + return strings.Trim(out, "\n") +} + +// StripHealth is Strip, and also the reported health status. Use it where the +// status is still settling and the test is not about it. +func StripHealth(out string) string { + return Strip(healthRegex.ReplaceAllString(out, `"health": "-stripped-",`)) +} + +// StripIPv6Lines drops every line carrying an IPv6 address. +func StripIPv6Lines(out string) string { + kept := []string{} + for line := range strings.SplitSeq(out, "\n") { + if !ipv6Regex.MatchString(line) { + kept = append(kept, line) + } + } + + return strings.Join(kept, "\n") +} diff --git a/testlib/tier.go b/testlib/tier.go new file mode 100644 index 0000000..2fab910 --- /dev/null +++ b/testlib/tier.go @@ -0,0 +1,43 @@ +package testlib + +import ( + "os" + "testing" +) + +// The environment a stage runs in. `just test-local` and `just test-e2e` set one +// each; `just test` sets neither, which is what makes it the middle stage. +const ( + EnvLocal = "INCUS_COMPOSE_TEST_LOCAL" + EnvE2E = "INCUS_COMPOSE_TEST_E2E" + EnvExamples = "INCUS_COMPOSE_TEST_EXAMPLES" +) + +// SkipLocal skips a test that needs a real Incus server. +func SkipLocal(t *testing.T) { + t.Helper() + + if os.Getenv(EnvLocal) != "" { + t.Skip("needs a real Incus: " + EnvLocal + " is set, run `just test`") + } +} + +// SkipE2E skips a slow test that stands up a fixture stack. Opposite polarity to +// SkipLocal: an end-to-end test runs only when asked for. +func SkipE2E(t *testing.T) { + t.Helper() + + if os.Getenv(EnvE2E) == "" { + t.Skip("long end-to-end test: set " + EnvE2E + "=1, or run `just test-e2e`") + } +} + +// SkipExamples skips a test that brings up a project from examples/. Same +// polarity as SkipE2E. +func SkipExamples(t *testing.T) { + t.Helper() + + if os.Getenv(EnvExamples) == "" { + t.Skip("examples test: set " + EnvExamples + "=1, or run `just test-examples`") + } +} diff --git a/testlib/tier_test.go b/testlib/tier_test.go new file mode 100644 index 0000000..a9fe580 --- /dev/null +++ b/testlib/tier_test.go @@ -0,0 +1,72 @@ +package testlib_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/lxc/incus-compose/testlib" +) + +// ran reports whether the body carried on past the guard. A skip unwinds its own +// goroutine, so reaching the next line is the only observable outcome. +func ran(t *testing.T, guard func(*testing.T)) bool { + t.Helper() + + reached := false + + t.Run("probe", func(t *testing.T) { + guard(t) + + reached = true + }) + + return reached +} + +// TestStageNames pins the literals: the other half of the contract is in +// just/test.just, which no Go test can read. +func TestStageNames(t *testing.T) { + assert.Equal(t, "INCUS_COMPOSE_TEST_LOCAL", testlib.EnvLocal, + "just/test.just sets this on test-local") + assert.Equal(t, "INCUS_COMPOSE_TEST_E2E", testlib.EnvE2E, + "just/test.just sets this on test-e2e") +} + +// TestSkipLocal: a daemon test skips in the stage with no daemon, runs elsewhere. +func TestSkipLocal(t *testing.T) { + t.Run("skips in the local stage", func(t *testing.T) { + t.Setenv(testlib.EnvLocal, "1") + assert.False(t, ran(t, testlib.SkipLocal)) + }) + + t.Run("runs when nothing is set", func(t *testing.T) { + t.Setenv(testlib.EnvLocal, "") + assert.True(t, ran(t, testlib.SkipLocal)) + }) + + t.Run("runs in the e2e stage", func(t *testing.T) { + t.Setenv(testlib.EnvLocal, "") + t.Setenv(testlib.EnvE2E, "1") + assert.True(t, ran(t, testlib.SkipLocal)) + }) +} + +// TestSkipE2E: the opposite polarity, an e2e test runs only when asked for. +func TestSkipE2E(t *testing.T) { + t.Run("skips when nothing is set", func(t *testing.T) { + t.Setenv(testlib.EnvE2E, "") + assert.False(t, ran(t, testlib.SkipE2E)) + }) + + t.Run("skips in the local stage", func(t *testing.T) { + t.Setenv(testlib.EnvLocal, "1") + t.Setenv(testlib.EnvE2E, "") + assert.False(t, ran(t, testlib.SkipE2E)) + }) + + t.Run("runs in the e2e stage", func(t *testing.T) { + t.Setenv(testlib.EnvE2E, "1") + assert.True(t, ran(t, testlib.SkipE2E)) + }) +} From aa12292f333fb77053e9f0e58ae5df6e2cb2f45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Wed, 12 Aug 2026 11:44:54 +0200 Subject: [PATCH 3/5] docs(agents): comments are not safeguards, enforce by code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Jochum --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 833bd2d..2e51424 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ discussable - always ask before guessing. - Code comments should be no longer than one line, unless they are required to cover complex unintuitive logic. - Never explain previous behaviour in comments. +- Comments are not safeguards, they are informal. An API is safe to use from several goroutines because it is mutex-free or confined to one, never because a comment says it is. - Commit messages should similarly be kept as short and to the point as possible, no need to summarize the whole issue. Keep the conventional `(): ` format from CONTRIBUTING.md. - Do not use `go vet`, `gci` or any of those diagnostics tools, use gopls. - You don't need to capture tests on your own use `just test-log` to get the last log. @@ -58,6 +59,7 @@ commands instead of raw `go` (see `just --list`). ## Working in this repo - Check existing patterns in the codebase before creating new ones. +- In most cases we do not enforce security by comments, we enforce by code and architecture. - Think through framework/library behavior before coding. - Keep code direct - no unnecessary intermediate variables; use `_` for unused parameters. - If cycling (same approach, no progress), stop and ask. From 320f59ed28393156ffaf86735403b64c417bd693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Thu, 13 Aug 2026 01:10:47 +0200 Subject: [PATCH 4/5] feat(tooling): update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Jochum --- .claude/commands/feedback.md | 2 +- .containerignore | 3 + .dockerignore | 1 + .gitignore | 6 +- just/fleet.just | 365 +++++++++++++++++++++++++++++++++++ just/test.just | 12 +- justfile | 1 + scripts/stress-test.sh | 155 --------------- {test => work}/logs/.gitkeep | 0 9 files changed, 378 insertions(+), 167 deletions(-) create mode 100644 .containerignore create mode 120000 .dockerignore create mode 100644 just/fleet.just delete mode 100755 scripts/stress-test.sh rename {test => work}/logs/.gitkeep (100%) diff --git a/.claude/commands/feedback.md b/.claude/commands/feedback.md index df1d71a..6718112 100644 --- a/.claude/commands/feedback.md +++ b/.claude/commands/feedback.md @@ -2,4 +2,4 @@ Task done. Give feedback on our teamwork (you and me) also from the perspective of a human teammate. There is no need write a handoff. -Write that to ./.feedback/$(date +%Y%m%d-%H%M%S).md (terse for me only it is gitignored). +Write that to ./work/feedback/$(date +%Y%m%d-%H%M%S).md (terse for me only it is gitignored). diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..ea2d14e --- /dev/null +++ b/.containerignore @@ -0,0 +1,3 @@ +# Scratch area: design notes, vendored sources, test logs and coverage +work/ +.git/ diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 0000000..092a75d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.containerignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index e86027d..d128db5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,8 @@ .mcp.json -test/logs/* -!test/logs/.gitkeep - -.feedback/* - work/ +!work/logs/.gitkeep # Its generated with random host only data, still no need to commit. /.env diff --git a/just/fleet.just b/just/fleet.just new file mode 100644 index 0000000..e4cba31 --- /dev/null +++ b/just/fleet.just @@ -0,0 +1,365 @@ +# Standing fleets to point CoreDNS at. + +# Builds a standing fleet and leaves it up. +# +# just fleet flat up 160 20 # 160 projects, 20 at a time +# just fleet nested up 160 20 +# just fleet flat down 20 # tear it down, 20 at a time +# +# The two topologies differ in one axis, and it is the axis worth isolating: +# +# flat three hosts on one network per project, so views scale with +# projects. What the design assumes. +# nested one gateway per network and two workers on one each, so no two +# instances share a network set - one view per instance, nothing +# collapsing. The pathological case for byView, which is views x names. +# +# Both stamp user.coredns.stress=true, so one scope_marker serves either. +# +# down forces out everything whose name starts with PREFIX, and it is the same +# PREFIX up built with, so nothing is looked up. +# +# Environment: +# +# PREFIX project name prefix, default "flat" or "stress" by topology +# NETWORKS bridges per project, nested only, default 3 +# SERVICE the shared fan-out name, flat only, default "web". Empty drops it +# OVN=0 opts out of OVN. On by default: projects then own OVN networks of +# their own rather than referencing bridges in the default project, +# which is the case netKey exists for - two projects can each own a +# network of the same name and they are genuinely different wires +# UPLINK the OVN uplink, default incusbr0. Needs ipv4.ovn.ranges set +# IMAGE instance image, POOL storage pool +[doc("Build or tear down a stress fleet: just fleet ...")] +fleet topology action *args: + #!/usr/bin/env bash + # -u only: the loops below lean on `test && action`, which is a non-zero + # statement whenever the test is false. + set -u + + topology="{{ topology }}" + action="{{ action }}" + + usage() { + echo "usage: just fleet up PROJECTS [CONCURRENCY]" >&2 + echo " just fleet down [CONCURRENCY]" >&2 + + exit 1 + } + + # Fixed per topology, because the shape is the experiment rather than a + # parameter of it. + case "${topology}" in + flat) + HOSTS=3 + default_prefix=flat + ;; + nested) + WORKERS=2 + NETWORKS="${NETWORKS:-3}" + default_prefix=stress + ;; + *) usage ;; + esac + + PREFIX="${PREFIX:-${default_prefix}}" + IMAGE="${IMAGE:-docker.io:library/nginx:alpine}" + POOL="${POOL:-default}" + UPLINK="${UPLINK:-incusbr0}" + MARKER="user.coredns.stress" + OVN="${OVN:-1}" + + RESULTS_DIR=$(mktemp -d) + + # flat: one network, three hosts on it, all answering to one shared name + # beside their own - three instances behind web. is what Render + # packs into one reply. One profile for all three: same network, so the same + # instance as far as visibility goes. + project_up_flat() { + local project=$1 + local h + + local -a svc=() + [[ -n "${SERVICE-web}" ]] && svc=(-c "user.label.coredns.service=${SERVICE-web}") + + incus network create "${net_args[@]}" "${project}-net1" "${ovn_type[@]}" || return 1 + + incus profile create "${inproj[@]}" "${project}-host" || return 1 + incus profile device add "${inproj[@]}" "${project}-host" root disk path=/ pool="${POOL}" || return 1 + incus profile device add "${inproj[@]}" "${project}-host" eth0 nic \ + network="${project}-net1" || return 1 + + for ((h = 1; h <= HOSTS; h++)); do + INCUS_PROJECT="${project}" incus launch "${IMAGE}" "host${h}" \ + -p "${project}-host" "${svc[@]}" || return 1 + done + } + + # nested: NETWORKS bridges, a gateway spanning all of them and two workers on + # one each, round-robin. Views then differ per instance, two workers on + # different networks cannot see each other, and the gateway exercises the + # join Gather only takes for a multi-homed name. + project_up_nested() { + local project=$1 + local n w on + local -a gw + + for ((n = 1; n <= NETWORKS; n++)); do + incus network create "${net_args[@]}" "${project}-net${n}" "${ovn_type[@]}" || return 1 + done + + # Only eth0 offers a default route. Every subsequent NIC is + # ipv4.gateway=none, or each one adds a default route of its own and an OCI + # instance egresses through whichever was applied last. + incus profile create "${inproj[@]}" "${project}-gw" || return 1 + incus profile device add "${inproj[@]}" "${project}-gw" root disk path=/ pool="${POOL}" || return 1 + + for ((n = 1; n <= NETWORKS; n++)); do + gw=() + ((n > 1)) && gw=(ipv4.gateway=none) + + incus profile device add "${inproj[@]}" "${project}-gw" "eth$((n - 1))" nic \ + network="${project}-net${n}" "${gw[@]}" || return 1 + done + + for ((w = 1; w <= WORKERS; w++)); do + on=$(((w - 1) % NETWORKS + 1)) + + incus profile create "${inproj[@]}" "${project}-w${w}" || return 1 + incus profile device add "${inproj[@]}" "${project}-w${w}" root disk path=/ pool="${POOL}" || return 1 + incus profile device add "${inproj[@]}" "${project}-w${w}" eth0 nic \ + network="${project}-net${on}" || return 1 + done + + # -p names the whole profile list, so these carry the root disk themselves + # and the default project's default profile is never involved. + INCUS_PROJECT="${project}" incus launch "${IMAGE}" gateway -p "${project}-gw" || return 1 + + for ((w = 1; w <= WORKERS; w++)); do + INCUS_PROJECT="${project}" incus launch "${IMAGE}" "worker${w}" \ + -p "${project}-w${w}" || return 1 + done + } + + # A project that owns its networks has to own its profiles too: a NIC device + # is validated when it is added, in the project the profile is in, so a + # profile in default naming a network the project owns fails to load. + project_up() { + local project=$1 + local owned=false + + ((OVN)) && owned=true + + inproj=() + net_args=() + ovn_type=() + + if ((OVN)); then + inproj=(--project "${project}") + net_args=(--project "${project}") + ovn_type=(--type ovn network="${UPLINK}") + fi + + incus project create "${project}" \ + -c "features.networks=${owned}" \ + -c "features.profiles=${owned}" \ + -c "${MARKER}=true" || return 1 + + "project_up_${topology}" "${project}" + } + + # The project goes first: Incus refuses to delete a network or a profile + # while anything still references it. + project_down() { + local project=$1 + local profile network + + echo yes | incus project delete --force "${project}" + + for profile in $(incus profile list -f csv 2>/dev/null | cut -d, -f1); do + [[ "${profile}" == "${project}-"* ]] || continue + + incus profile delete "${profile}" 2>/dev/null + done + + for network in $(incus network list -f csv 2>/dev/null | cut -d, -f1); do + [[ "${network}" == "${project}-"* ]] || continue + + incus network delete "${network}" 2>/dev/null + done + } + + run_timed() { + local label=$1 project=$2 + shift 2 + + local start end duration status + + start=$(date +%s.%N) + if "$@" >"${RESULTS_DIR}/${project}.log" 2>&1; then + status="ok" + else + status="FAIL" + fi + end=$(date +%s.%N) + duration=$(awk "BEGIN{printf \"%.2f\", ${end}-${start}}") + + echo "${duration}" >"${RESULTS_DIR}/${project}.time" + echo "${status}" >"${RESULTS_DIR}/${project}.status" + printf " [%s] %-10s %-20s %6ss\n" "${status}" "${label}" "${project}" "${duration}" + } + + summarize() { + local -a times=() + local f + + for f in "${RESULTS_DIR}"/*.time; do + [[ -e "$f" ]] || continue + times+=("$(cat "$f")") + done + + [[ ${#times[@]} -eq 0 ]] && return 0 + + local fail_count + fail_count=$(grep -l "^FAIL$" "${RESULTS_DIR}"/*.status 2>/dev/null | wc -l) + + printf "%s\n" "${times[@]}" | awk -v fails="$fail_count" ' + { sum += $1; if (NR==1 || $1max) max=$1; n++ } + END { + printf " projects=%-4d fails=%-4s avg=%6.2fs min=%6.2fs max=%6.2fs\n", n, fails, sum/n, min, max + } + ' + } + + # Rolling rather than batched. Waiting for a whole batch leaves the slowest + # project running on its own while the daemon has nothing else to do, which + # reads as incusd idling halfway through every batch. + rolling() { + local concurrency=$1 label=$2 fn=$3 + shift 3 + + local project pid + local -a pids=() alive=() + + for project in "$@"; do + run_timed "${label}" "${project}" "${fn}" "${project}" & + pids+=("$!") + + if ((${#pids[@]} >= concurrency)); then + wait -n + + alive=() + for pid in "${pids[@]}"; do + kill -0 "${pid}" 2>/dev/null && alive+=("${pid}") + done + + pids=("${alive[@]}") + fi + done + + for pid in "${pids[@]}"; do wait "${pid}"; done + } + + # Checked once rather than discovered PROJECTS times. An OVN network is + # allocated an uplink address out of ipv4.ovn.ranges, and a fresh Incus sets + # no such range - so every create fails for the same reason, one project at a + # time. + check_ovn() { + if [[ -z "$(incus network get "${UPLINK}" ipv4.ovn.ranges 2>/dev/null)" ]]; then + echo "uplink ${UPLINK} has no ipv4.ovn.ranges, so no OVN network can be allocated one." >&2 + echo "give it a slice of its own subnet, clear of the DHCP pool:" >&2 + echo " incus network get ${UPLINK} ipv4.address" >&2 + echo " incus network set ${UPLINK} ipv4.ovn.ranges -" >&2 + + exit 1 + fi + + if ! incus network list -f csv 2>/dev/null | grep -q "^br-int,"; then + echo "no br-int: OVS/OVN is not running on this daemon." >&2 + echo "incus-compose's setup-nested-incus.sh installs it, but only with -o:" >&2 + echo " ./scripts/setup-nested-incus.sh -o ..." >&2 + echo "The uplink's ipv4.ovn.ranges is in its preseed either way, so the range" >&2 + echo "being set is not evidence that OVN is there." >&2 + + exit 1 + fi + } + + case "${action}" in + up) + projects="${3:-}" + concurrency="${4:-8}" + + [[ -n "${projects}" ]] || usage + + ((OVN)) && check_ovn + + if [[ "${topology}" == flat ]]; then + echo "Building ${projects} projects x 1 network x ${HOSTS} hosts" + echo " topology every host on the same network, so one view per project" + else + echo "Building ${projects} projects x ${NETWORKS} networks x $((WORKERS + 1)) instances" + echo " topology 1 gateway on every network, ${WORKERS} workers on one each" + fi + + echo " image ${IMAGE}" + echo " marker ${MARKER}=true" + echo " logs ${RESULTS_DIR}" + echo + + wanted=() + for ((p = 1; p <= projects; p++)); do + wanted+=("$(printf '%s-%03d' "${PREFIX}" "${p}")") + done + + start=$(date +%s.%N) + rolling "${concurrency}" up project_up "${wanted[@]}" + end=$(date +%s.%N) + + echo + summarize + printf " wall-clock=%.2fs\n" "$(awk "BEGIN{print ${end}-${start}}")" + echo + + if [[ "${topology}" == flat ]]; then + echo "Instances: $((projects * HOSTS)), views: ${projects}" + echo "Check with: PREFIX=${PREFIX} NAMES='host1 host2' just coredns-verify-dns 53 ${PREFIX}" + else + echo "Instances: $((projects * (WORKERS + 1))), NICs: $((projects * (NETWORKS + WORKERS)))" + echo "Check with: PREFIX=${PREFIX} just coredns-verify-dns 53 ${PREFIX}" + fi + + echo "Tear down with: just fleet ${topology} down ${concurrency}" + ;; + + down) + concurrency="${3:-8}" + + doomed=() + for project in $(incus project list -f csv 2>/dev/null | cut -d, -f1 | sed 's/ (current)//'); do + [[ "${project}" == "${PREFIX}"* ]] || continue + + doomed+=("${project}") + done + + if ((${#doomed[@]} == 0)); then + echo "no ${PREFIX}* projects to remove" + + exit 0 + fi + + echo "Removing ${#doomed[@]} projects, ${concurrency} at a time" + echo " logs ${RESULTS_DIR}" + echo + + start=$(date +%s.%N) + rolling "${concurrency}" down project_down "${doomed[@]}" + end=$(date +%s.%N) + + echo + summarize + printf " wall-clock=%.2fs\n" "$(awk "BEGIN{print ${end}-${start}}")" + ;; + + *) usage ;; + esac diff --git a/just/test.just b/just/test.just index 5b27f80..40843b8 100644 --- a/just/test.just +++ b/just/test.just @@ -13,9 +13,9 @@ v_test_timeout := env("TEST_TIMEOUT", "20m") test folder="./..." *args: lint mkdir -p test/logs export DATE=`date +%Y%m%d-%H%M%S`; \ - gotestsum --hide-summary=skipped --format testname --jsonfile=test/logs/${DATE}.json --packages={{ folder }} \ - --post-run-command "bash -c 'echo; echo Slowest tests; gotestsum tool slowest --num 10 --jsonfile test/logs/${DATE}.json'" \ - -- -race -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -covermode atomic -coverprofile test/logs/${DATE}-cover.out -v "${@:2}" + gotestsum --hide-summary=skipped --format testname --jsonfile=work/logs/${DATE}.json --packages={{ folder }} \ + --post-run-command "bash -c 'echo; echo Slowest tests; gotestsum tool slowest --num 10 --jsonfile work/logs/${DATE}.json'" \ + -- -race -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -covermode atomic -coverprofile work/logs/${DATE}-cover.out -v "${@:2}" # Run local unit-tests, incus-facing tests are skipped. [env("INCUS_COMPOSE_TEST_LOCAL", "1")] @@ -43,7 +43,7 @@ test-all folder="./..." *args: # Run the tests with a race detector. test-race folder="./..." *args: mkdir -p test/logs - gotestsum --hide-summary=skipped --jsonfile=test/logs/`date +%Y%m%d-%H%M%S`.json --packages={{ folder }} -- -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -race -v "${@:2}" + gotestsum --hide-summary=skipped --jsonfile=work/logs/`date +%Y%m%d-%H%M%S`.json --packages={{ folder }} -- -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -race -v "${@:2}" # Update snapshots for long running tests. [env("INCUS_COMPOSE_TEST_E2E", "1")] @@ -70,13 +70,13 @@ update-snapshots folder="./..." *args: # On a terminal it follows until Ctrl-C, so it can be started while a run is # going; piped it reads once and exits. The newest log is picked at startup, so # a run launched after this is not the one being followed. -[doc("Plain text of the newest test/logs/*.json. Follows on a terminal")] +[doc("Plain text of the newest work/logs/*.json. Follows on a terminal")] test-log pattern="" test="": #!/usr/bin/env bash set -euo pipefail log="" - for candidate in test/logs/*.json; do + for candidate in work/logs/*.json; do [[ -f "${candidate}" ]] || continue [[ -z "${log}" || "${candidate}" -nt "${log}" ]] && log="${candidate}" done diff --git a/justfile b/justfile index a8be1f5..fdb807c 100644 --- a/justfile +++ b/justfile @@ -23,6 +23,7 @@ v_test_procs := env("TEST_PROCS", "2") import 'just/mod.just' import 'just/test.just' import 'just/build.just' +import 'just/fleet.just' import 'just/incus.just' [private] diff --git a/scripts/stress-test.sh b/scripts/stress-test.sh deleted file mode 100755 index eb1e73c..0000000 --- a/scripts/stress-test.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: ./stress-test.sh [PAIRS_PER_BATCH] [BATCHES] -# -# Runs PAIRS_PER_BATCH concurrent (simplenginx, twoservices) pairs per batch, -# waits for the batch to finish, then repeats for BATCHES batches. Comparing -# batch 1's timings/probe to batch N's timings/probe isolates degradation -# that accumulates across runs from plain intra-batch concurrency contention. - -simplenginx() { - project_name=$1 - network_name=$2 - - incus project create "${project_name}" - INCUS_PROJECT="${project_name}" incus profile device add default root disk path=/ pool=default - - INCUS_PROJECT="${project_name}" incus network create "${network_name}" - INCUS_PROJECT="${project_name}" incus profile device add default eth0 nic network="${network_name}" - - INCUS_PROJECT="${project_name}" incus launch docker.io:library/nginx:alpine web - - INCUS_PROJECT="${project_name}" incus stop web - INCUS_PROJECT="${project_name}" incus remove web - - echo "yes" | incus project rm -f "${project_name}" - incus network rm "${network_name}" -} - -twoservices() { - project_name=$1 - network_name=$2 - - incus project create "${project_name}" - INCUS_PROJECT="${project_name}" incus profile device add default root disk path=/ pool=default - - INCUS_PROJECT="${project_name}" incus network create "${network_name}" - INCUS_PROJECT="${project_name}" incus profile device add default eth0 nic network="${network_name}" - - INCUS_PROJECT="${project_name}" incus launch docker.io:nginxinc/nginx-unprivileged:alpine ic-healthd - INCUS_PROJECT="${project_name}" incus launch docker.io:nginxinc/nginx-unprivileged:alpine web1 - INCUS_PROJECT="${project_name}" incus launch docker.io:nginxinc/nginx-unprivileged:alpine web2 - - INCUS_PROJECT="${project_name}" incus stop ic-healthd - INCUS_PROJECT="${project_name}" incus stop web1 - INCUS_PROJECT="${project_name}" incus stop web2 - - INCUS_PROJECT="${project_name}" incus remove ic-healthd - INCUS_PROJECT="${project_name}" incus remove web1 - INCUS_PROJECT="${project_name}" incus remove web2 - - echo "yes" | incus project rm -f "${project_name}" - incus network rm "${network_name}" -} - -set -u - -if [[ $# -eq 0 ]]; then - echo "Usage: $0 [PAIRS_PER_BATCH] [BATCHES]" - echo " PAIRS_PER_BATCH concurrent (simplenginx, twoservices) pairs per batch (default: 5)" - echo " BATCHES number of sequential batches (default: 4)" - echo "No arguments given, running with defaults (5 pairs x 4 batches)." - echo -fi - -PAIRS="${1:-5}" # concurrent (simplenginx, twoservices) pairs per batch -BATCHES="${2:-4}" # number of sequential batches - -RESULTS_DIR=$(mktemp -d) - -run_timed() { - local label=$1 func=$2 project=$3 network=$4 - local start end duration status - - start=$(date +%s.%N) - if "$func" "$project" "$network" >"${RESULTS_DIR}/${project}.log" 2>&1; then - status="ok" - else - status="FAIL" - fi - end=$(date +%s.%N) - duration=$(awk "BEGIN{printf \"%.2f\", ${end}-${start}}") - - echo "${duration}" >"${RESULTS_DIR}/${project}.time" - echo "${status}" >"${RESULTS_DIR}/${project}.status" - printf " [%s] %-12s %-20s %6ss\n" "${status}" "${label}" "${project}" "${duration}" -} - -summarize_batch() { - local label=$1 batch=$2 - local -a times=() - for f in "${RESULTS_DIR}/b${batch}-${label}"*.time; do - [[ -e "$f" ]] || continue - times+=("$(cat "$f")") - done - - local fail_count - fail_count=$(grep -l "^FAIL$" "${RESULTS_DIR}/b${batch}-${label}"*.status 2>/dev/null | wc -l) - - printf "%s\n" "${times[@]}" | awk -v label="$label" -v fails="$fail_count" ' - { sum += $1; if (NR==1 || $1max) max=$1; n++ } - END { - if (n == 0) { print " "label": no data"; exit } - printf " %-12s runs=%-3d fails=%-3s avg=%6.2fs min=%6.2fs max=%6.2fs\n", label, n, fails, sum/n, min, max - } - ' -} - -declare -a batch_durations - -echo "Logs in ${RESULTS_DIR}" -echo -for batch in $(seq 1 "${BATCHES}"); do - echo "=== Batch ${batch}/${BATCHES} (${PAIRS} concurrent pairs) ===" - - pids=() - batch_start=$(date +%s.%N) - for ((i = 1; i <= PAIRS; i++)); do - run_timed "simplenginx" simplenginx "b${batch}-simplenginx${i}" "b${batch}-simple${i}" & - pids+=("$!") - - run_timed "twoservices" twoservices "b${batch}-twoservices${i}" "b${batch}-two${i}" & - pids+=("$!") - done - - for pid in "${pids[@]}"; do - wait "${pid}" - done - batch_end=$(date +%s.%N) - batch_duration=$(awk "BEGIN{printf \"%.2f\", ${batch_end}-${batch_start}}") - - batch_durations+=("${batch_duration}") - - summarize_batch "simplenginx" "${batch}" - summarize_batch "twoservices" "${batch}" - echo " batch wall-clock=${batch_duration}s" - echo -done - -echo "=== Batch-over-batch comparison ===" -printf "%-8s %-14s\n" "batch" "wall-clock" -for ((b = 1; b <= BATCHES; b++)); do - printf "%-8s %-14s\n" "${b}" "${batch_durations[$((b - 1))]}s" -done - -failed_status=$(grep -l "^FAIL$" "${RESULTS_DIR}"/*.status 2>/dev/null || true) -if [[ -n "${failed_status}" ]]; then - echo - echo "Failures detected, matching logs:" - for f in ${failed_status}; do - echo "${f%.status}.log" - done -fi - -echo -echo "Per-run logs and timings kept in: ${RESULTS_DIR} remove manually when done" diff --git a/test/logs/.gitkeep b/work/logs/.gitkeep similarity index 100% rename from test/logs/.gitkeep rename to work/logs/.gitkeep From 93193ce2f76a53b1dac6f21b6cc6df24b5f65761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jochum?= Date: Thu, 13 Aug 2026 06:04:23 +0200 Subject: [PATCH 5/5] fix(tooling): fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Jochum --- .gitignore | 1 - just/test.just | 4 ++-- test/snapshots/e2e/TestDNSCnameAliasAcrossProjects | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index d128db5..476b405 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ .mcp.json work/ -!work/logs/.gitkeep # Its generated with random host only data, still no need to commit. /.env diff --git a/just/test.just b/just/test.just index 40843b8..fa21e94 100644 --- a/just/test.just +++ b/just/test.just @@ -11,7 +11,7 @@ v_test_timeout := env("TEST_TIMEOUT", "20m") # Run tests against nested Incus, includes direct incus tests. [env("INCUS_COMPOSE_IMAGE_CACHE", "incus-compose-tests-cache")] test folder="./..." *args: lint - mkdir -p test/logs + mkdir -p work/logs export DATE=`date +%Y%m%d-%H%M%S`; \ gotestsum --hide-summary=skipped --format testname --jsonfile=work/logs/${DATE}.json --packages={{ folder }} \ --post-run-command "bash -c 'echo; echo Slowest tests; gotestsum tool slowest --num 10 --jsonfile work/logs/${DATE}.json'" \ @@ -42,7 +42,7 @@ test-all folder="./..." *args: # Run the tests with a race detector. test-race folder="./..." *args: - mkdir -p test/logs + mkdir -p work/logs gotestsum --hide-summary=skipped --jsonfile=work/logs/`date +%Y%m%d-%H%M%S`.json --packages={{ folder }} -- -parallel {{ v_test_procs }} -timeout {{ v_test_timeout }} -race -v "${@:2}" # Update snapshots for long running tests. diff --git a/test/snapshots/e2e/TestDNSCnameAliasAcrossProjects b/test/snapshots/e2e/TestDNSCnameAliasAcrossProjects index 7c9b85e..dd226db 100644 --- a/test/snapshots/e2e/TestDNSCnameAliasAcrossProjects +++ b/test/snapshots/e2e/TestDNSCnameAliasAcrossProjects @@ -3,4 +3,3 @@ address=/service-db/-stripped- address=/service-db2/-stripped- cname=db.mydomain.lan,my-db cname=db2.mydomain.lan,my-db2 -