(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))))