From 93c4b340e48531893816aae0d5167f336aad4b8d Mon Sep 17 00:00:00 2001 From: ajet Date: Sat, 19 Sep 2026 05:50:34 -0900 Subject: [PATCH] making franz happy --- .gitea/workflows/ci.yml | 77 ++++++++++++++++ README.md | 92 +++++++++++++++++++ bb.edn | 27 ------ deps.edn | 4 +- .../Dockerfile | 0 .../README.md | 24 ++--- .../docker-compose.coverage.yml | 4 +- .../docker-compose.yml | 0 .../integration_test.bb | 0 .../vibe/integration/coverage.clj | 0 script/coverage | 9 ++ script/coverage-report | 7 ++ {scripts => script}/coverage_report.clj | 6 +- script/integration-coverage | 6 ++ script/integration-test | 79 ++++++++++++++++ script/repl | 13 +++ script/test | 7 ++ script/unit-coverage | 8 ++ script/unit-test | 6 ++ scripts/integration_tasks.clj | 79 ---------------- 20 files changed, 323 insertions(+), 125 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 README.md delete mode 100644 bb.edn rename {integration-tests => integration-test}/Dockerfile (100%) rename {integration-tests => integration-test}/README.md (70%) rename {integration-tests => integration-test}/docker-compose.coverage.yml (80%) rename {integration-tests => integration-test}/docker-compose.yml (100%) rename {integration-tests => integration-test}/integration_test.bb (100%) rename {integration-tests => integration-test}/vibe/integration/coverage.clj (100%) create mode 100755 script/coverage create mode 100755 script/coverage-report rename {scripts => script}/coverage_report.clj (96%) create mode 100755 script/integration-coverage create mode 100755 script/integration-test create mode 100755 script/repl create mode 100755 script/test create mode 100755 script/unit-coverage create mode 100755 script/unit-test delete mode 100644 scripts/integration_tasks.clj diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d32733b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + unit: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "11" + + - uses: DeLaGuardo/setup-clojure@13.4.0 + with: + cli: latest + + - uses: actions/cache@v4 + with: + path: | + ~/.m2/repository + ~/.gitlibs + key: deps-${{ runner.os }}-${{ hashFiles('deps.edn') }} + restore-keys: deps-${{ runner.os }}- + + - run: script/unit-test + + integration: + name: Integration tests + coverage + runs-on: ubuntu-latest + needs: unit + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "11" + + - uses: DeLaGuardo/setup-clojure@13.4.0 + with: + cli: latest + bb: latest + + - uses: actions/cache@v4 + with: + path: | + ~/.m2/repository + ~/.gitlibs + key: deps-${{ runner.os }}-${{ hashFiles('deps.edn') }} + restore-keys: deps-${{ runner.os }}- + + # The integration suite drives `docker compose` and `docker buildx`, so the + # runner must expose a Docker daemon (e.g. /var/run/docker.sock mounted into + # job containers, or a host-mode runner). Fail fast with a clear message. + - name: Check Docker + run: | + docker version + docker compose version + docker buildx version + + # Unit + integration under Cloverage, then the merged report. + - run: script/coverage + + # Gitea Actions only supports upload-artifact v3 (v4+ needs GitHub's artifact backend). + - uses: actions/upload-artifact@v3 + if: always() + with: + name: coverage + path: target/coverage/ + if-no-files-found: ignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf1807b --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# vibe + +A small Clojure HTTP API built on [Ring](https://github.com/ring-clojure/ring) (Jetty) and +[reitit](https://github.com/metosin/reitit), with Swagger UI docs, unit tests, dockerized +integration tests, and joint line coverage. + +## Requirements + +- [Clojure CLI](https://clojure.org/guides/install_clojure) (tools.deps) and JDK 11+ +- [Babashka](https://babashka.org/) for merging coverage reports (`script/coverage-report`) +- Docker, for the integration tests only + +## Running + +```bash +clojure -X:run +``` + +The server listens on port `3000` by default; set `PORT` to change it: + +```bash +PORT=8080 clojure -X:run +``` + +Or build and run the container: + +```bash +docker build -t vibe . +docker run --rm -p 3000:3000 vibe +``` + +## API + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/` | Plain-text landing message | +| `GET` | `/api/ping` | Health check, returns `pong` | +| `GET` | `/api/add?x=2&y=3` | Adds two numbers, returns EDN: `{:result 5}` (400 if either is missing or not a number) | +| `GET` | `/api-docs` | Swagger UI | +| `GET` | `/swagger.json` | OpenAPI/Swagger spec | + +```bash +curl localhost:3000/api/ping # pong +curl 'localhost:3000/api/add?x=2&y=3' # {:result 5} +``` + +## Development + +Every task is a bash script in `script/`. Start an nREPL server (with CIDER middleware) on the port in `.nrepl-port`, or `3000` if that +file doesn't exist: + +```bash +script/repl +``` + +### Tests + +| Task | What it does | +|------|--------------| +| `script/unit-test` | Runs the Clojure tests in `test/` | +| `script/integration-test` | Builds the app image, starts it on a random port, runs the [integration suite](integration-test/README.md) against it, then tears it down | +| `script/test` | Unit tests, then integration tests (stops at the first failure) | + +### Coverage + +Line coverage via [Cloverage](https://github.com/cloverage/cloverage): + +| Task | Output | +|------|--------| +| `script/unit-coverage` | `target/coverage/unit/` | +| `script/integration-coverage` | `target/coverage/integration/` | +| `script/coverage` | Runs both, then writes the merged report to `target/coverage/merged/` | + +Each report directory has an `index.html` and an `lcov.info`. See +[integration-test/README.md](integration-test/README.md) for how integration coverage is +collected. + + +## Project layout + +``` +src/vibe/ + core.clj Router, Ring app, and server startup + /routes.clj Each route group (health, math, ...) owns its route subtree + /core.clj Business logic for the group, with no HTTP concerns +test/ Unit tests +integration-test/ Dockerized integration tests and coverage harness +script/ Bash entry points (test, coverage, repl, ...) and coverage helpers +``` + +To add a new group, create `vibe..routes` with a `routes` fn that returns a reitit route +vector, then add it to the router in `src/vibe/core.clj`. diff --git a/bb.edn b/bb.edn deleted file mode 100644 index 6743686..0000000 --- a/bb.edn +++ /dev/null @@ -1,27 +0,0 @@ -{:paths ["integration-tests" "scripts"] - :override-builtin true - :tasks - {:requires ([integration-tasks :as it]) - unit-test {:doc "Run the Clojure unit tests (test/ directory)." - :task (shell "clojure" "-M:test")} - test {:doc "Run the unit tests, then the integration tests (stops at the first failure)." - :task (do (run 'unit-test) - (run 'integration-test))} - repl {:doc "Start an nREPL server with dev dependencies" - :task (shell "clojure" "-A:dev" "-M" "-m" "nrepl.cmdline" - "--middleware" "[cider.nrepl/cider-middleware]" - "--port" (try (slurp ".nrepl-port") (catch Exception _ "3000")))} - integration-test {:doc "Run the dockerized integration tests in isolation (builds the app image, starts it on a random port, runs the test suite against it, then tears everything down)." - :task (it/run-integration {})} - unit-coverage {:doc "Run the unit tests under Cloverage; reports go to target/coverage/unit." - :task (shell "clojure" "-M:cov" "-m" "cloverage.coverage" - "--src-ns-path" "src" "--test-ns-path" "test" - "--lcov" "--html" "--output" "target/coverage/unit")} - integration-coverage {:doc "Run the dockerized integration tests against a Cloverage-instrumented app; reports go to target/coverage/integration." - :task (it/run-integration {:coverage? true})} - coverage-report {:doc "Merge the unit and integration line-coverage results into target/coverage/merged and print a summary." - :task (shell "bb" "scripts/coverage_report.clj")} - coverage {:doc "Run the full suite (unit + integration) with line coverage and write the joint report to target/coverage/merged." - :task (do (run 'unit-coverage) - (run 'integration-coverage) - (run 'coverage-report))}}} diff --git a/deps.edn b/deps.edn index 94d3143..458f9c6 100644 --- a/deps.edn +++ b/deps.edn @@ -1,4 +1,4 @@ -{:paths ["src" "resources" "integration-tests"] +{:paths ["src" "resources" "integration-test"] :deps {org.clojure/clojure {:mvn/version "1.12.0"} ring/ring-jetty-adapter {:mvn/version "1.12.2"} @@ -16,7 +16,7 @@ :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} :main-opts ["-m" "cognitect.test-runner"]} ;; Line coverage via Cloverage. Only adds the dep + test path; the entry points - ;; (unit: cloverage.coverage, integration: vibe.integration.coverage) are chosen in bb.edn. + ;; (unit: cloverage.coverage, integration: vibe.integration.coverage) are chosen in script/unit-coverage and script/integration-coverage. :cov {:extra-paths ["test"] :extra-deps {cloverage/cloverage {:mvn/version "1.2.4"}}} diff --git a/integration-tests/Dockerfile b/integration-test/Dockerfile similarity index 100% rename from integration-tests/Dockerfile rename to integration-test/Dockerfile diff --git a/integration-tests/README.md b/integration-test/README.md similarity index 70% rename from integration-tests/README.md rename to integration-test/README.md index 0898570..4abfc28 100644 --- a/integration-tests/README.md +++ b/integration-test/README.md @@ -7,7 +7,7 @@ Dockerized integration tests for the vibe server. Run the full test suite (builds the app, starts it, runs tests): ```bash -cd integration-tests +cd integration-test docker-compose up --abort-on-container-exit ``` @@ -16,21 +16,21 @@ docker-compose up --abort-on-container-exit If your vibe app is running locally on port 3000: ```bash -cd integration-tests +cd integration-test docker run --rm -v "$(pwd)/integration_test.bb:/integration_test.bb:ro" babashka/babashka:latest bb /integration_test.bb http://host.docker.internal:3000 ``` Or run directly on a host with babashka installed: ```bash -cd integration-tests +cd integration-test bb integration_test.bb http://localhost:3000 ``` -Or from the repo root, via the `bb.edn` task: +Or from the repo root, via the script: ```bash -bb integration-test +script/integration-test ``` ### Options @@ -44,7 +44,7 @@ bb integration-test Example — watch a local server every 2 seconds: ```bash -bb integration-test http://localhost:3000 --watch --interval 2000 +bb integration-test/integration_test.bb http://localhost:3000 --watch --interval 2000 ``` ## What's Tested @@ -64,10 +64,10 @@ unit tests, the integration tests, and both combined. From the repo root: | Task | What it does | Output | |------|--------------|--------| -| `bb unit-coverage` | Runs `test/` under Cloverage | `target/coverage/unit/` | -| `bb integration-coverage` | Runs the integration suite against a Cloverage-instrumented `vibe-app` container | `target/coverage/integration/` | -| `bb coverage-report` | Merges the two `lcov.info` files, prints a per-file table | `target/coverage/merged/` | -| `bb coverage` | All three, in order (the full suite with a joint report) | all of the above | +| `script/unit-coverage` | Runs `test/` under Cloverage | `target/coverage/unit/` | +| `script/integration-coverage` | Runs the integration suite against a Cloverage-instrumented `vibe-app` container | `target/coverage/integration/` | +| `script/coverage-report` | Merges the two `lcov.info` files, prints a per-file table | `target/coverage/merged/` | +| `script/coverage` | All three, in order (the full suite with a joint report) | all of the above | Each directory contains an `index.html` (open it in a browser) and an `lcov.info`. In the merged report a line counts as covered if either suite hit it; the HTML shows per-line hit @@ -75,6 +75,6 @@ counts per suite (`u:` unit, `i:` integration). How the integration numbers are collected: `docker-compose.coverage.yml` starts the app via `vibe.integration.coverage`, which lets Cloverage instrument `src/` and then serves real -traffic until it receives SIGTERM. `bb integration-coverage` stops the container +traffic until it receives SIGTERM. `script/integration-coverage` stops the container (`docker compose stop`) so the report gets written, then copies it out with `docker compose cp`. -Plain `bb integration-test` / `bb test` are unchanged and run the uninstrumented app. +Plain `script/integration-test` / `script/test` are unchanged and run the uninstrumented app. diff --git a/integration-tests/docker-compose.coverage.yml b/integration-test/docker-compose.coverage.yml similarity index 80% rename from integration-tests/docker-compose.coverage.yml rename to integration-test/docker-compose.coverage.yml index 67404c1..fb7fa77 100644 --- a/integration-tests/docker-compose.coverage.yml +++ b/integration-test/docker-compose.coverage.yml @@ -1,5 +1,5 @@ -# Overlay for `bb integration-coverage`: runs vibe-app under Cloverage instead of plainly. -# Reports land in /app/target/coverage/integration inside the container; bb.edn copies +# Overlay for `script/integration-coverage`: runs vibe-app under Cloverage instead of plainly. +# Reports land in /app/target/coverage/integration inside the container; script/integration-test copies # them out after the app is stopped (SIGTERM is what triggers the report). services: vibe-app: diff --git a/integration-tests/docker-compose.yml b/integration-test/docker-compose.yml similarity index 100% rename from integration-tests/docker-compose.yml rename to integration-test/docker-compose.yml diff --git a/integration-tests/integration_test.bb b/integration-test/integration_test.bb similarity index 100% rename from integration-tests/integration_test.bb rename to integration-test/integration_test.bb diff --git a/integration-tests/vibe/integration/coverage.clj b/integration-test/vibe/integration/coverage.clj similarity index 100% rename from integration-tests/vibe/integration/coverage.clj rename to integration-test/vibe/integration/coverage.clj diff --git a/script/coverage b/script/coverage new file mode 100755 index 0000000..cc991e8 --- /dev/null +++ b/script/coverage @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Run the full suite (unit + integration) with line coverage and write the joint +# report to target/coverage/merged. +set -euo pipefail +cd "$(dirname "$0")/.." + +script/unit-coverage +script/integration-coverage +script/coverage-report diff --git a/script/coverage-report b/script/coverage-report new file mode 100755 index 0000000..78e70ef --- /dev/null +++ b/script/coverage-report @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Merge the unit and integration line-coverage results into target/coverage/merged +# and print a summary. Requires Babashka. +set -euo pipefail +cd "$(dirname "$0")/.." + +exec bb script/coverage_report.clj "$@" diff --git a/scripts/coverage_report.clj b/script/coverage_report.clj similarity index 96% rename from scripts/coverage_report.clj rename to script/coverage_report.clj index 3753a7f..af829b6 100644 --- a/scripts/coverage_report.clj +++ b/script/coverage_report.clj @@ -1,7 +1,7 @@ #!/usr/bin/env bb ;; Merges the unit and integration LCOV files into one line-coverage report. ;; -;; bb scripts/coverage_report.clj [--out target/coverage/merged] +;; bb script/coverage_report.clj [--out target/coverage/merged] ;; ;; Reads target/coverage/{unit,integration}/lcov.info (either may be missing), prints a ;; per-file table (unit / integration / merged) and writes the merged result to @@ -40,7 +40,7 @@ ;; Cloverage's SF: paths are usually project-relative already (src/vibe/core.clj); fall ;; back to the source roots in case they are classpath-relative. (some (fn [dir] (let [f (io/file dir file)] (when (.isFile f) (slurp f)))) - [nil "src" "test" "integration-tests"])) + [nil "src" "test" "integration-test"])) (defn esc [s] (-> s (str/replace "&" "&") (str/replace "<" "<") (str/replace ">" ">"))) @@ -104,7 +104,7 @@ details{margin:1em 0}summary{cursor:pointer;font-weight:600} :when (.exists (io/file path))] [k (parse-lcov (slurp path))]))] (when (empty? per-suite) - (println "No lcov.info found; run `bb unit-coverage` and/or `bb integration-coverage` first.") + (println "No lcov.info found; run `script/unit-coverage` and/or `script/integration-coverage` first.") (System/exit 1)) (let [merged (apply merge-with (fn [a b] (merge-with + a b)) (vals per-suite)) cols (keys per-suite) diff --git a/script/integration-coverage b/script/integration-coverage new file mode 100755 index 0000000..8a30a8c --- /dev/null +++ b/script/integration-coverage @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the dockerized integration tests against a Cloverage-instrumented app; +# reports go to target/coverage/integration. +set -euo pipefail + +COVERAGE=1 exec "$(dirname "$0")/integration-test" diff --git a/script/integration-test b/script/integration-test new file mode 100755 index 0000000..98a39e8 --- /dev/null +++ b/script/integration-test @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Run the dockerized integration tests in isolation: builds the app image, starts it on a +# random port, runs the suite against it, then tears everything down and exits with the +# suite's exit code. +# +# With COVERAGE=1 (see script/integration-coverage) the app runs under Cloverage +# (docker-compose.coverage.yml); after the suite finishes the app is stopped gracefully so +# it writes its report, which is copied to target/coverage/integration. +set -euo pipefail +cd "$(dirname "$0")/.." +root=$PWD + +# Picks a random port in [lo, hi] that's currently free on this host; retries on +# collision since another process could grab it first. +free_port_in_range() { + local lo=$1 hi=$2 port attempt + for attempt in $(seq 20); do + port=$((lo + RANDOM % (hi - lo + 1))) + # A successful connect means something is already listening. + if ! (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then + echo "$port" + return 0 + fi + done + echo "no free port found in range $lo-$hi" >&2 + return 1 +} + +port=$(free_port_in_range 30000 40000) +echo "[test] starting vibe-app on random port $port${COVERAGE:+ (under coverage instrumentation)}" + +compose=(docker compose) +if [[ -n "${COVERAGE:-}" ]]; then + compose+=(-f docker-compose.yml -f docker-compose.coverage.yml) +fi + +# Works around an LXC/AppArmor bug in runc (opencontainers/runc#5007) that fails RUN steps +# during image build; a docker-container builder with apparmor disabled sidesteps it. +builder=vibe-builder +if ! docker buildx inspect "$builder" >/dev/null 2>&1; then + docker buildx create --name "$builder" \ + --driver docker-container \ + --buildkitd-config buildkitd.toml +fi +docker buildx use "$builder" + +cd integration-test +export VIBE_PORT=$port + +teardown() { + local status=$? + trap - EXIT + "${compose[@]}" down -v || true + exit "$status" +} +trap teardown EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +# `run` returns the test container's own exit code; `up --abort-on-container-exit` can leak +# the app's SIGTERM exit (143) during teardown instead. +status=0 +"${compose[@]}" --ansi never --progress plain run --build --rm integration-test || status=$? + +if [[ -n "${COVERAGE:-}" ]]; then + # SIGTERM is what makes Cloverage write its report, so stop (not kill) the app and pull + # the report out of the stopped container before `down` removes it. + out_dir=$root/target/coverage + echo "[test] stopping vibe-app to flush coverage report" + "${compose[@]}" stop -t 120 vibe-app || true + rm -rf "$out_dir/integration" + mkdir -p "$out_dir" + if ! "${compose[@]}" cp vibe-app:/app/target/coverage/integration "$out_dir" \ + || [[ ! -f $out_dir/integration/lcov.info ]]; then + echo "[test] WARNING: no integration coverage report was produced" + fi +fi + +exit "$status" diff --git a/script/repl b/script/repl new file mode 100755 index 0000000..a48afb1 --- /dev/null +++ b/script/repl @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Start an nREPL server with dev dependencies, on the port in .nrepl-port (default 3000). +set -euo pipefail +cd "$(dirname "$0")/.." + +port=3000 +if [[ -f .nrepl-port ]]; then + port=$(tr -d '[:space:]' < .nrepl-port) +fi + +exec clojure -A:dev -M -m nrepl.cmdline \ + --middleware "[cider.nrepl/cider-middleware]" \ + --port "$port" diff --git a/script/test b/script/test new file mode 100755 index 0000000..8456e79 --- /dev/null +++ b/script/test @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Run the unit tests, then the integration tests (stops at the first failure). +set -euo pipefail +cd "$(dirname "$0")/.." + +script/unit-test +script/integration-test diff --git a/script/unit-coverage b/script/unit-coverage new file mode 100755 index 0000000..348e807 --- /dev/null +++ b/script/unit-coverage @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Run the unit tests under Cloverage; reports go to target/coverage/unit. +set -euo pipefail +cd "$(dirname "$0")/.." + +exec clojure -M:cov -m cloverage.coverage \ + --src-ns-path src --test-ns-path test \ + --lcov --html --output target/coverage/unit diff --git a/script/unit-test b/script/unit-test new file mode 100755 index 0000000..7407ab9 --- /dev/null +++ b/script/unit-test @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the Clojure unit tests (test/ directory). Extra args go to the test runner. +set -euo pipefail +cd "$(dirname "$0")/.." + +exec clojure -M:test "$@" diff --git a/scripts/integration_tasks.clj b/scripts/integration_tasks.clj deleted file mode 100644 index bee1f0b..0000000 --- a/scripts/integration_tasks.clj +++ /dev/null @@ -1,79 +0,0 @@ -(ns integration-tasks - "Shared implementation for the dockerized integration bb tasks (see bb.edn)." - (:require [babashka.process :refer [shell]])) - -(defn run-integration - "Builds the app image, starts it on a random port, runs the integration suite against - it, then tears everything down and exits with the suite's exit code. - - With :coverage? the app runs under Cloverage (docker-compose.coverage.yml); after the - suite finishes the app is stopped gracefully so it writes its report, which is copied - to target/coverage/integration." - [{:keys [coverage?]}] - (let [free-port-in-range - ;; Picks a random port in [lo, hi] that's currently bindable on this - ;; host; retries on collision since another process could grab it first. - (fn [lo hi] - (loop [attempts 20] - (let [port (+ lo (rand-int (inc (- hi lo))))] - (if (try - (.close (java.net.ServerSocket. port)) - true - (catch java.io.IOException _ false)) - port - (if (pos? attempts) - (recur (dec attempts)) - (throw (ex-info "no free port found in range" {:lo lo :hi hi}))))))) - port (free-port-in-range 30000 40000) - _ (println (format "[test] starting vibe-app on random port %d%s" port - (if coverage? " (under coverage instrumentation)" ""))) - builder-name "vibe-builder" - compose (into ["docker" "compose"] - (when coverage? - ["-f" "docker-compose.yml" "-f" "docker-compose.coverage.yml"])) - ;; Works around an LXC/AppArmor bug in runc (opencontainers/runc#5007) that - ;; fails RUN steps during image build; a docker-container builder with - ;; apparmor disabled sidesteps it. - _ (let [{:keys [exit]} (shell {:continue true} "docker" "buildx" "inspect" builder-name)] - (when-not (zero? exit) - (shell "docker" "buildx" "create" "--name" builder-name - "--driver" "docker-container" - "--buildkitd-config" "buildkitd.toml"))) - _ (shell "docker" "buildx" "use" builder-name) - ;; Runs from a JVM shutdown hook (normal exit and Ctrl-C), where - ;; babashka.process/shell fails: it registers its own shutdown hook, - ;; which the JVM rejects with "Shutdown in progress". Plain - ;; ProcessBuilder has no such hook and lets us wait for `down` to finish. - teardown (fn [] - (-> (doto (ProcessBuilder. ^java.util.List (vec (concat compose ["down" "-v"]))) - (.directory (java.io.File. "integration-tests")) - (.inheritIO)) - .start - .waitFor)) - _ (.addShutdownHook (Runtime/getRuntime) (Thread. teardown)) - {:keys [exit]} (apply shell {:dir "integration-tests" :continue true - :extra-env {"VIBE_PORT" (str port)}} - ;; `run` returns the test container's own exit code; `up --abort-on-container-exit` - ;; can leak the app's SIGTERM exit (143) during teardown instead. - (concat compose ["--ansi" "never" "--progress" "plain" - "run" "--build" "--rm" "integration-test"]))] - (when coverage? - ;; SIGTERM is what makes Cloverage write its report, so stop (not kill) the app - ;; and pull the report out of the stopped container before `down` removes it. - (let [out-dir (java.io.File. "target/coverage") - dest (java.io.File. out-dir "integration")] - (println "[test] stopping vibe-app to flush coverage report") - (apply shell {:dir "integration-tests" :continue true} - (concat compose ["stop" "-t" "120" "vibe-app"])) - (when (.exists dest) - (run! (fn [^java.io.File f] (.delete f)) (reverse (file-seq dest)))) - (.mkdirs out-dir) - (let [{cp-exit :exit} (apply shell {:dir "integration-tests" :continue true} - (concat compose ["cp" "vibe-app:/app/target/coverage/integration" - (.getAbsolutePath out-dir)]))] - (when-not (and (zero? cp-exit) (.exists (java.io.File. dest "lcov.info"))) - (println "[test] WARNING: no integration coverage report was produced"))))) - ;; Only exit early on failure so chained tasks (`bb coverage`) can carry on after a pass; - ;; the teardown hook still runs when bb exits. - (when-not (zero? exit) - (System/exit exit))))