|
| 1 | +#!/usr/bin/env bats |
| 2 | +# ============================================================================ |
| 3 | +# BATS tests for _compose_run_with_summary in dream-cli. |
| 4 | +# ============================================================================ |
| 5 | +# Guards against re-breakage of PR #406 (compose wrapper surfaces a compact |
| 6 | +# summary on success, an error banner + grep'd keywords + log path on |
| 7 | +# failure, and propagates the compose exit code so `dream restart/stop/start` |
| 8 | +# aborts under `set -e`). |
| 9 | +# |
| 10 | +# Test strategy: |
| 11 | +# - Extract _compose_run_with_summary + its helper (log/success/warn/ |
| 12 | +# log_error) text from dream-cli. |
| 13 | +# - PATH-inject a `docker` stub that can be told to exit 0 / exit 1 with |
| 14 | +# error-keyword output / exit 1 with no-keyword output. |
| 15 | +# - Run the wrapper and assert on printed output + return code. |
| 16 | + |
| 17 | +load '../bats/bats-support/load' |
| 18 | +load '../bats/bats-assert/load' |
| 19 | + |
| 20 | +setup() { |
| 21 | + export TMPDIR_TEST="$BATS_TEST_TMPDIR" |
| 22 | + mkdir -p "$TMPDIR_TEST/bin" |
| 23 | + |
| 24 | + # Color vars used by log/success/warn/log_error — stub to empty so |
| 25 | + # assert_output matches plain strings without escape sequences. |
| 26 | + export RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC="" |
| 27 | + |
| 28 | + # Minimal logger helpers that match dream-cli's line 45–50 definitions. |
| 29 | + # We redefine rather than extract because (a) they're trivial and (b) |
| 30 | + # dream-cli's real definitions pull in color vars we've already stubbed. |
| 31 | + log() { echo "[dream] $1"; } |
| 32 | + success() { echo "✓ $1"; } |
| 33 | + warn() { echo "⚠ $1"; } |
| 34 | + log_error() { echo "✗ $1" >&2; } |
| 35 | + export -f log success warn log_error |
| 36 | + |
| 37 | + # Extract _compose_run_with_summary from dream-cli. |
| 38 | + local _cli="$BATS_TEST_DIRNAME/../../dream-cli" |
| 39 | + eval "$(awk '/^_compose_run_with_summary\(\) \{/,/^\}$/' "$_cli")" |
| 40 | + |
| 41 | + # docker stub — behavior driven by DOCKER_STUB_MODE env var: |
| 42 | + # success → exit 0 with benign stdout |
| 43 | + # fail-keyword→ exit 1, stdout contains 'error', 'failed', 'unhealthy', 'dependency' |
| 44 | + # fail-nomatch→ exit 1, stdout has no error keywords |
| 45 | + cat > "$TMPDIR_TEST/bin/docker" <<'STUB' |
| 46 | +#!/usr/bin/env bash |
| 47 | +echo "DOCKER_ARGS: $*" >> "$DOCKER_CALL_LOG" |
| 48 | +case "${DOCKER_STUB_MODE:-success}" in |
| 49 | + success) |
| 50 | + echo "Network dream-net created" |
| 51 | + echo "Container dream-foo started" |
| 52 | + exit 0 |
| 53 | + ;; |
| 54 | + fail-keyword) |
| 55 | + echo "Container dream-foo starting" |
| 56 | + echo "Error response from daemon: service dream-foo failed to start" |
| 57 | + echo "unhealthy container dream-bar" |
| 58 | + echo "dependency dream-baz not ready" |
| 59 | + exit 1 |
| 60 | + ;; |
| 61 | + fail-nomatch) |
| 62 | + echo "Container dream-foo starting" |
| 63 | + echo "network busy (try again later)" |
| 64 | + exit 1 |
| 65 | + ;; |
| 66 | + *) |
| 67 | + echo "unknown DOCKER_STUB_MODE: $DOCKER_STUB_MODE" >&2 |
| 68 | + exit 127 |
| 69 | + ;; |
| 70 | +esac |
| 71 | +STUB |
| 72 | + chmod +x "$TMPDIR_TEST/bin/docker" |
| 73 | + export PATH="$TMPDIR_TEST/bin:$PATH" |
| 74 | + export DOCKER_CALL_LOG="$TMPDIR_TEST/docker.log" |
| 75 | + : > "$DOCKER_CALL_LOG" |
| 76 | +} |
| 77 | + |
| 78 | +teardown() { |
| 79 | + rm -rf "$TMPDIR_TEST/bin" "$TMPDIR_TEST/docker.log" |
| 80 | +} |
| 81 | + |
| 82 | +# ── success path ──────────────────────────────────────────────────────────── |
| 83 | + |
| 84 | +@test "wrapper: success prints compact '<verb> — done' banner and returns 0" { |
| 85 | + export DOCKER_STUB_MODE=success |
| 86 | + run _compose_run_with_summary "Restarting all services" up -d |
| 87 | + assert_success |
| 88 | + assert_output --partial "Restarting all services..." |
| 89 | + assert_output --partial "Restarting all services — done" |
| 90 | +} |
| 91 | + |
| 92 | +@test "wrapper: success removes the compose log tmpfile" { |
| 93 | + export DOCKER_STUB_MODE=success |
| 94 | + # Track how many mktemp-produced files exist before & after. We rely on |
| 95 | + # $TMPDIR being the system tmp — the wrapper uses `mktemp` with no args. |
| 96 | + local before=$(find /tmp -maxdepth 1 -name 'tmp.*' -type f 2>/dev/null | wc -l) |
| 97 | + run _compose_run_with_summary "Starting all services" up -d |
| 98 | + assert_success |
| 99 | + local after=$(find /tmp -maxdepth 1 -name 'tmp.*' -type f 2>/dev/null | wc -l) |
| 100 | + # Zero net new temp files (create + rm). |
| 101 | + [ "$after" -le "$before" ] |
| 102 | +} |
| 103 | + |
| 104 | +@test "wrapper: success does NOT print error banner or log path" { |
| 105 | + export DOCKER_STUB_MODE=success |
| 106 | + run _compose_run_with_summary "Starting all services" up -d |
| 107 | + assert_success |
| 108 | + refute_output --partial "Full compose output:" |
| 109 | + refute_output --partial "failed:" |
| 110 | +} |
| 111 | + |
| 112 | +# ── failure path, matching keywords (PR #406) ─────────────────────────────── |
| 113 | + |
| 114 | +@test "wrapper: failure prints error banner, matched lines, and log path" { |
| 115 | + export DOCKER_STUB_MODE=fail-keyword |
| 116 | + run _compose_run_with_summary "Restarting service x" up -d x |
| 117 | + assert_failure |
| 118 | + assert_output --partial "Restarting service x failed:" |
| 119 | + # At least one error-keyword line should be surfaced (indented two spaces). |
| 120 | + assert_output --partial "Error response from daemon" |
| 121 | + assert_output --partial "Full compose output:" |
| 122 | +} |
| 123 | + |
| 124 | +@test "wrapper: failure propagates the compose exit code (1)" { |
| 125 | + export DOCKER_STUB_MODE=fail-keyword |
| 126 | + run _compose_run_with_summary "Restarting" up -d |
| 127 | + # docker stub exits 1; wrapper must return 1. |
| 128 | + [ "$status" -eq 1 ] |
| 129 | +} |
| 130 | + |
| 131 | +@test "wrapper: failure preserves the compose log file (not auto-removed)" { |
| 132 | + export DOCKER_STUB_MODE=fail-keyword |
| 133 | + run _compose_run_with_summary "Restarting" up -d |
| 134 | + assert_failure |
| 135 | + # Extract the "Full compose output: /tmp/tmp.XXXXXX" path from output. |
| 136 | + local log_line |
| 137 | + log_line=$(printf '%s\n' "$output" | grep -E 'Full compose output:' | head -1) |
| 138 | + [ -n "$log_line" ] |
| 139 | + local log_path="${log_line##*: }" |
| 140 | + [ -f "$log_path" ] |
| 141 | + rm -f "$log_path" |
| 142 | +} |
| 143 | + |
| 144 | +# ── failure path, zero keyword matches (nounset/pipefail-hardening) ───────── |
| 145 | + |
| 146 | +# These two tests must execute the wrapper under `set -euo pipefail` because |
| 147 | +# the `|| warn "(no error keywords matched...)"` branch only fires when |
| 148 | +# grep's exit-1 propagates through the pipeline — and only pipefail makes |
| 149 | +# grep the pipeline's final exit code. dream-cli line 6 IS `set -euo pipefail` |
| 150 | +# in production; bats' setup() doesn't inherit that, so we set it explicitly |
| 151 | +# via the subshell. |
| 152 | + |
| 153 | +@test "wrapper: failure with no keyword match fires the warn fallback (under pipefail)" { |
| 154 | + export DOCKER_STUB_MODE=fail-nomatch |
| 155 | + # Re-extract the wrapper inside a pipefail-enabled subshell. |
| 156 | + run bash -c ' |
| 157 | + set -euo pipefail |
| 158 | + RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC="" |
| 159 | + log() { echo "[dream] $1"; } |
| 160 | + success() { echo "✓ $1"; } |
| 161 | + warn() { echo "⚠ $1"; } |
| 162 | + log_error() { echo "✗ $1" >&2; } |
| 163 | + eval "$(awk "/^_compose_run_with_summary\(\) \{/,/^\}$/" "'"$BATS_TEST_DIRNAME/../../dream-cli"'")" |
| 164 | + _compose_run_with_summary "Stopping service y" down y |
| 165 | + ' |
| 166 | + assert_failure |
| 167 | + assert_output --partial "Stopping service y failed:" |
| 168 | + assert_output --partial "(no error keywords matched in compose log)" |
| 169 | + assert_output --partial "Full compose output:" |
| 170 | +} |
| 171 | + |
| 172 | +@test "wrapper: failure with no keyword match still propagates exit code" { |
| 173 | + export DOCKER_STUB_MODE=fail-nomatch |
| 174 | + run bash -c ' |
| 175 | + set -euo pipefail |
| 176 | + RED="" GREEN="" YELLOW="" BLUE="" CYAN="" NC="" |
| 177 | + log() { echo "[dream] $1"; } |
| 178 | + success() { echo "✓ $1"; } |
| 179 | + warn() { echo "⚠ $1"; } |
| 180 | + log_error() { echo "✗ $1" >&2; } |
| 181 | + eval "$(awk "/^_compose_run_with_summary\(\) \{/,/^\}$/" "'"$BATS_TEST_DIRNAME/../../dream-cli"'")" |
| 182 | + _compose_run_with_summary "Stopping" down |
| 183 | + ' |
| 184 | + [ "$status" -eq 1 ] |
| 185 | +} |
| 186 | + |
| 187 | +# ── docker compose args passthrough ───────────────────────────────────────── |
| 188 | + |
| 189 | +@test "wrapper: all args after verb are passed to docker compose" { |
| 190 | + export DOCKER_STUB_MODE=success |
| 191 | + run _compose_run_with_summary "Up" -f extra.yml up -d my-service |
| 192 | + assert_success |
| 193 | + # Stub records its argv to DOCKER_CALL_LOG; first arg must be `compose` |
| 194 | + # because the wrapper calls `docker compose <args>`. |
| 195 | + run cat "$DOCKER_CALL_LOG" |
| 196 | + assert_output --partial "DOCKER_ARGS: compose --progress quiet -f extra.yml up -d my-service" |
| 197 | +} |
| 198 | + |
| 199 | +# ── propagation into callers (cmd_restart/cmd_stop/cmd_start) ─────────────── |
| 200 | + |
| 201 | +@test "wrapper: caller returns wrapper's non-zero exit (smoke)" { |
| 202 | + export DOCKER_STUB_MODE=fail-keyword |
| 203 | + # Simulate `cmd_restart`: wrapper is the last statement, so its exit |
| 204 | + # code becomes the function's exit code. |
| 205 | + _simulated_caller() { |
| 206 | + _compose_run_with_summary "Restarting all services" up -d |
| 207 | + } |
| 208 | + run _simulated_caller |
| 209 | + [ "$status" -eq 1 ] |
| 210 | +} |
| 211 | + |
| 212 | +@test "wrapper: caller returns 0 on success (smoke)" { |
| 213 | + export DOCKER_STUB_MODE=success |
| 214 | + _simulated_caller() { |
| 215 | + _compose_run_with_summary "Restarting all services" up -d |
| 216 | + } |
| 217 | + run _simulated_caller |
| 218 | + [ "$status" -eq 0 ] |
| 219 | +} |
0 commit comments