From bdcb3e0796540237c450645c67be24fa3646bcd4 Mon Sep 17 00:00:00 2001 From: okash1n <48118431+okash1n@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:54:16 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=E3=82=B7=E3=83=8A=E3=83=AA=E3=82=AA?= =?UTF-8?q?=E3=81=AE=20setup.sh=20=E5=A4=B1=E6=95=97=E6=99=82=E3=81=AB?= =?UTF-8?q?=E5=87=BA=E5=8A=9B=E6=9C=AB=E5=B0=BE=E3=82=92=20CI=20=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=81=B8=E6=AE=8B=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit シナリオテストは run_setup / run_setup_update の出力を >/dev/null 2>&1 で 捨てているため、setup.sh が失敗しても CI ログには「(setup failed)」か、 `|| rc=$?` のない呼び出しでは set -e による runner の無言中断しか残らず、 原因を追えなかった。macOS ジョブの先頭シナリオで 2026-09-06〜08 に 3 回 起きた一過性失敗(約 20 秒で setup.sh が非ゼロ終了)がこの状態で、 再実行でしか回復できていない。 - tests/helpers.sh: runner の元 stdout を fd 3 に複製し、setup.sh の出力を 一時ファイルへ取ってから stdout に再生する。非ゼロ終了時は末尾 _TEST_SETUP_LOG_TAIL 行(既定 20)を fd 3 へ出す。呼び出し側の fd 1/2 リダイレクトや `$(...)` 取り込みは影響を受けない - tests/unit/test-helpers-run-setup.sh: 偽の setup.sh で失敗時の tail 出力、 成功時の無出力、run_setup_update の引数と取り込み出力の不変を検証 ローカル確認: SCENARIO_GROUP=core 14 PASS / 1 SKIP(bash4 既定)、 新規単体テスト 3/3、shellcheck、/bin/bash -n Claude-Session: https://claude.ai/code/session_01C9mrbbXQgV9fJ8Zy5UoEYc --- tests/helpers.sh | 36 ++++++++++- tests/unit/test-helpers-run-setup.sh | 90 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test-helpers-run-setup.sh diff --git a/tests/helpers.sh b/tests/helpers.sh index f2d4787..881d3af 100644 --- a/tests/helpers.sh +++ b/tests/helpers.sh @@ -107,6 +107,38 @@ teardown_test_env() { # Ensure cleanup on unexpected exit (set -e abort, signal, etc.) trap 'teardown_test_env' EXIT +# --------------------------------------------------------------------------- +# Scenario callers discard setup.sh output (`>/dev/null 2>&1`), so a setup +# failure used to leave no trace in the CI log — only "(setup failed)" or, when +# the caller has no `|| rc=$?`, a bare set -e abort of the whole runner. Keep a +# copy of the runner's original stdout on fd 3 and print the tail of a failed +# setup.sh run there; the caller's fd 1/2 redirections do not affect fd 3. +# --------------------------------------------------------------------------- +if ! { true >&3; } 2>/dev/null; then + exec 3>&1 +fi +_TEST_SETUP_LOG_TAIL="${_TEST_SETUP_LOG_TAIL:-20}" + +# _run_setup_logged [setup.sh args...] +# Runs setup.sh, replays its combined output on stdout (so callers that +# capture or grep the output keep working) and, on a non-zero exit, prints the +# last $_TEST_SETUP_LOG_TAIL lines to fd 3. Returns the exit code of setup.sh. +_run_setup_logged() { + local log rc=0 + log="$(mktemp)" + bash "$PROJECT_DIR/setup.sh" "$@" >"$log" 2>&1 || rc=$? + if [[ "$rc" -ne 0 ]]; then + { + printf ' [setup.sh exited %s: %s] last %s lines:\n' \ + "$rc" "$*" "$_TEST_SETUP_LOG_TAIL" + tail -n "$_TEST_SETUP_LOG_TAIL" "$log" | sed 's/^/ | /' + } >&3 + fi + cat "$log" + rm -f "$log" + return "$rc" +} + # --------------------------------------------------------------------------- # run_setup - Run setup.sh with given args in the test environment # @@ -115,14 +147,14 @@ trap 'teardown_test_env' EXIT # Returns the exit code of setup.sh # --------------------------------------------------------------------------- run_setup() { - bash "$PROJECT_DIR/setup.sh" --non-interactive --language=en "$@" 2>&1 + _run_setup_logged --non-interactive --language=en "$@" } # --------------------------------------------------------------------------- # run_setup_update - Run setup.sh in update mode # --------------------------------------------------------------------------- run_setup_update() { - bash "$PROJECT_DIR/setup.sh" --update --non-interactive "$@" 2>&1 + _run_setup_logged --update --non-interactive "$@" } # --------------------------------------------------------------------------- diff --git a/tests/unit/test-helpers-run-setup.sh b/tests/unit/test-helpers-run-setup.sh new file mode 100644 index 0000000..c1f225e --- /dev/null +++ b/tests/unit/test-helpers-run-setup.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# tests/unit/test-helpers-run-setup.sh - run_setup / run_setup_update diagnostics +# +# Sourced by run-unit-tests.sh (helpers.sh already loaded). Uses a fake +# setup.sh under a private PROJECT_DIR so nothing real is deployed. + +_hrs_tmp="$(mktemp -d)" +mkdir -p "$_hrs_tmp/project" +cat > "$_hrs_tmp/project/setup.sh" <<'EOF' +#!/bin/bash +for i in 1 2 3 4 5; do printf 'line %s\n' "$i"; done +printf 'args: %s\n' "$*" >&2 +exit "${HRS_FAKE_RC:-0}" +EOF + +{ + test_name="helpers: run_setup replays setup.sh output on stdout and prints a failure tail on fd 3" + _hrs_rc=0 + ( + # shellcheck disable=SC2034 # read by run_setup / run_setup_update + PROJECT_DIR="$_hrs_tmp/project" + _TEST_SETUP_LOG_TAIL=3 + export HRS_FAKE_RC=7 + run_setup --profile=minimal \ + >"$_hrs_tmp/stdout" 2>"$_hrs_tmp/stderr" 3>"$_hrs_tmp/fd3" + ) || _hrs_rc=$? + if [[ "$_hrs_rc" -eq 7 ]] \ + && grep -q '^line 1$' "$_hrs_tmp/stdout" \ + && grep -q '^args: --non-interactive --language=en --profile=minimal$' \ + "$_hrs_tmp/stdout" \ + && [[ ! -s "$_hrs_tmp/stderr" ]] \ + && grep -q 'setup.sh exited 7: --non-interactive --language=en --profile=minimal' \ + "$_hrs_tmp/fd3" \ + && grep -q '| line 4$' "$_hrs_tmp/fd3" \ + && grep -q '| args: ' "$_hrs_tmp/fd3" \ + && ! grep -q 'line 1' "$_hrs_tmp/fd3"; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="helpers: run_setup stays quiet on fd 3 when setup.sh succeeds" + rm -f "$_hrs_tmp/stdout" "$_hrs_tmp/stderr" "$_hrs_tmp/fd3" + _hrs_rc=0 + ( + # shellcheck disable=SC2034 # read by run_setup / run_setup_update + PROJECT_DIR="$_hrs_tmp/project" + export HRS_FAKE_RC=0 + run_setup --profile=minimal \ + >"$_hrs_tmp/stdout" 2>"$_hrs_tmp/stderr" 3>"$_hrs_tmp/fd3" + ) || _hrs_rc=$? + if [[ "$_hrs_rc" -eq 0 ]] \ + && grep -q '^line 5$' "$_hrs_tmp/stdout" \ + && [[ ! -s "$_hrs_tmp/stderr" ]] \ + && [[ ! -s "$_hrs_tmp/fd3" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="helpers: run_setup_update passes --update --non-interactive and keeps the caller's output capture intact" + rm -f "$_hrs_tmp/stdout" "$_hrs_tmp/fd3" + _hrs_rc=0 + _hrs_out="" + ( + # shellcheck disable=SC2034 # read by run_setup / run_setup_update + PROJECT_DIR="$_hrs_tmp/project" + export HRS_FAKE_RC=3 + # The same shape scenarios use: capture the output while fd 3 goes elsewhere. + _out="$(run_setup_update --dry-run 2>&1 3>"$_hrs_tmp/fd3")" || _rc=$? + printf '%s' "$_out" > "$_hrs_tmp/stdout" + exit "${_rc:-0}" + ) || _hrs_rc=$? + if [[ "$_hrs_rc" -eq 3 ]] \ + && grep -q '^args: --update --non-interactive --dry-run$' "$_hrs_tmp/stdout" \ + && ! grep -q 'setup.sh exited' "$_hrs_tmp/stdout" \ + && grep -q 'setup.sh exited 3: --update --non-interactive --dry-run' \ + "$_hrs_tmp/fd3"; then + pass "$test_name" + else + fail "$test_name" + fi +} + +rm -rf "$_hrs_tmp" +unset _hrs_tmp _hrs_rc _hrs_out