This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 207b2b4e6 [tools][ci] Fix the bats suite's boundary with the scripts
it tests (#1081)
207b2b4e6 is described below
commit 207b2b4e6683705a9f7077c0f0363ac2a49669af
Author: Weiqing Yang <[email protected]>
AuthorDate: Wed Sep 2 04:03:54 2026 -0700
[tools][ci] Fix the bats suite's boundary with the scripts it tests (#1081)
Generated-by: Claude Code 2.1.251 (Claude Opus 5)
---
.github/workflows/ci.yml | 41 +++++++
tools/install.sh | 23 +++-
tools/test/integration/build_help.bats | 6 +-
tools/test/integration/dry_run.bats | 10 +-
tools/test/integration/dry_run_extra.bats | 8 +-
tools/test/integration/err_trap.bats | 4 +-
tools/test/integration/help.bats | 4 +-
tools/test/integration/venv_dir_validation.bats | 4 +-
tools/test/run.sh | 44 ++++++-
tools/test/unit/edit_plan_quote.bats | 11 +-
tools/test/unit/install_sh_trap_isolation.bats | 91 +++++++++++++++
tools/test/unit/spawn_site_interpreter.bats | 126 +++++++++++++++++++++
tools/test/unit/ut_sh_flink_flag_scope.bats | 24 ++--
tools/test/unit/ut_sh_flink_version_supported.bats | 16 +--
tools/test/unit/ut_sh_version_default.bats | 18 +--
15 files changed, 364 insertions(+), 66 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 416aaa7cb..e260bacbd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -68,6 +68,47 @@ jobs:
- name: Run tools script tests
run: bash tools/test/run.sh
+ # install.sh and ut.sh both run on the bash macOS ships at /bin/bash for
their
+ # own users: install.sh is documented as `curl ... | bash`, and ut.sh is
+ # invoked by path from the macOS jobs below. run.sh pins `bash` to the newer
+ # interpreter the harness itself needs, and that pin reaches the scripts
under
+ # test as well, so this leg points them back at the one they ship against.
+ tools_script_tests_bash32:
+ name: tools script tests (macos-latest, subject on bash 3.2)
+ runs-on: macos-latest
+ # Declared once for the whole job so the check below and the run below read
+ # the same value. Setting it on the run step alone would let a misspelled
+ # key pass the check and then leave the suite spawning the subjects on the
+ # harness bash, which is the green-over-nothing outcome the check exists to
+ # prevent.
+ env:
+ FLINK_AGENTS_SUT_BASH: /bin/bash
+ steps:
+ - uses: actions/checkout@v4
+ # Only the subjects move; the harness keeps the version its own
+ # assertions need.
+ - name: Install bash 4.1+ for the harness
+ run: brew install bash
+ - name: Confirm the subject interpreter is the bash macOS ships
+ run: |
+ set -eu
+ echo "PATH bash: $(command -v bash)"
+ bash --version | head -1
+ : "${FLINK_AGENTS_SUT_BASH:?is not set, so run.sh would spawn the
subjects on the harness bash and this job would cover nothing}"
+ echo "subject: $FLINK_AGENTS_SUT_BASH"
+ "$FLINK_AGENTS_SUT_BASH" --version | head -1
+ major="$("$FLINK_AGENTS_SUT_BASH" -c 'printf %s
"${BASH_VERSINFO[0]}"')"
+ if [ "$major" != "3" ]; then
+ echo "ERROR: $FLINK_AGENTS_SUT_BASH on this runner is bash
${major}.x," >&2
+ echo "not 3.x. This job exists to run the scripts under test on
the" >&2
+ echo "bash that macOS ships. On a ${major}.x interpreter it
repeats" >&2
+ echo "the default macOS job and covers nothing, so it fails
here" >&2
+ echo "rather than report a green result it did not earn." >&2
+ exit 1
+ fi
+ - name: Run tools script tests with the subjects on the bash macOS ships
+ run: bash tools/test/run.sh
+
build_backend_tests:
name: ut-build-backend
runs-on: ubuntu-latest
diff --git a/tools/install.sh b/tools/install.sh
index 3f9033be0..dab4649be 100755
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -38,11 +38,19 @@ cleanup_tmpfiles() {
rm -rf "$f" 2>/dev/null || true
done
}
-trap cleanup_tmpfiles EXIT
-# Some interactive read combinations (notably `read -e` under stdin
redirection)
-# can swallow SIGINT, leaving the user pressing Ctrl+C with no effect. Install
-# an explicit INT trap so Ctrl+C always lands.
-trap 'die_cancelled' INT
+# Traps belong to a run, not to a source. The test suite and the e2e scripts
+# source this file under FLINK_AGENTS_INSTALL_SH_NO_RUN=1 to call individual
+# functions, and a top-level `trap` replaces the handlers whoever sourced us
+# already installed: bats in particular reports a failing or skipped result
from
+# inside its own EXIT trap, so displacing it makes that result vanish from the
+# output.
+if [[ "${FLINK_AGENTS_INSTALL_SH_NO_RUN:-0}" != "1" ]]; then
+ trap cleanup_tmpfiles EXIT
+ # Some interactive read combinations (notably `read -e` under stdin
+ # redirection) can swallow SIGINT, leaving the user pressing Ctrl+C with no
+ # effect. Install an explicit INT trap so Ctrl+C always lands.
+ trap 'die_cancelled' INT
+fi
mktempfile() {
local f
@@ -379,7 +387,10 @@ on_error() {
} >&2
exit "$rc"
}
-trap 'on_error $? $LINENO "$BASH_COMMAND"' ERR
+# Armed only for a run, for the reason given at the EXIT and INT traps above.
+if [[ "${FLINK_AGENTS_INSTALL_SH_NO_RUN:-0}" != "1" ]]; then
+ trap 'on_error $? $LINENO "$BASH_COMMAND"' ERR
+fi
INSTALL_STAGE_TOTAL=5
INSTALL_STAGE_CURRENT=0
diff --git a/tools/test/integration/build_help.bats
b/tools/test/integration/build_help.bats
index 5fc5ccfd2..bb9e99658 100644
--- a/tools/test/integration/build_help.bats
+++ b/tools/test/integration/build_help.bats
@@ -21,7 +21,7 @@
BUILD_SCRIPT="${BATS_TEST_DIRNAME}/../../build.sh"
@test "build --help prints usage and exits 0" {
- run bash "$BUILD_SCRIPT" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$BUILD_SCRIPT" --help
[ "$status" -eq 0 ]
[[ "$output" == *"Build Flink Agents Java and Python artifacts"* ]] ||
false
@@ -31,14 +31,14 @@ BUILD_SCRIPT="${BATS_TEST_DIRNAME}/../../build.sh"
}
@test "build -h prints usage and exits 0" {
- run bash "$BUILD_SCRIPT" -h
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$BUILD_SCRIPT" -h
[ "$status" -eq 0 ]
[[ "$output" == *"Usage:"* ]] || false
}
@test "build rejects an unknown option with usage" {
- run bash "$BUILD_SCRIPT" --no-such-option
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$BUILD_SCRIPT" --no-such-option
[ "$status" -eq 1 ]
[[ "$output" == *"Error: Unknown option '--no-such-option'"* ]] || false
diff --git a/tools/test/integration/dry_run.bats
b/tools/test/integration/dry_run.bats
index f16982140..fc6b740b0 100644
--- a/tools/test/integration/dry_run.bats
+++ b/tools/test/integration/dry_run.bats
@@ -16,7 +16,7 @@ esac
}
@test "--dry-run --non-interactive prints plan and makes no external calls" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --non-interactive
[ "$status" -eq 0 ]
case "$output" in *"Installation plan"*) ;; *) false ;; esac
case "$output" in *"Dry run complete"*) ;; *) false ;; esac
@@ -26,7 +26,7 @@ esac
}
@test "--dry-run --install-flink --non-interactive shows Install Flink: Yes" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
[ "$status" -eq 0 ]
case "$output" in *"Install Flink"*) ;; *) false ;; esac
case "$output" in *"Yes"*) ;; *) false ;; esac
@@ -35,20 +35,20 @@ esac
@test "INSTALL_DIR=. does not produce a double-slash FLINK_HOME (review
feedback guard)" {
cd "$BATS_TEST_TMPDIR"
- run env INSTALL_DIR="." bash "${BATS_TEST_DIRNAME}/../../install.sh"
--dry-run --install-flink --non-interactive
+ run env INSTALL_DIR="." "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
[ "$status" -eq 0 ]
case "$output" in *".//flink-"*) false ;; *) ;; esac
}
@test "INSTALL_DIR with trailing slash does not produce double-slash (review
#7b)" {
- run env INSTALL_DIR="/tmp/flink-test/" bash
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
+ run env INSTALL_DIR="/tmp/flink-test/" "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
[ "$status" -eq 0 ]
case "$output" in *"//"*) false ;; *) ;; esac
case "$output" in *"/tmp/flink-test"*) ;; *) false ;; esac
}
@test "INSTALL_DIR with consecutive slashes is collapsed (review #7b)" {
- run env INSTALL_DIR="/tmp//flink-test" bash
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
+ run env INSTALL_DIR="/tmp//flink-test" "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --install-flink
--non-interactive
[ "$status" -eq 0 ]
case "$output" in *"//flink-"*) false ;; *) ;; esac
}
diff --git a/tools/test/integration/dry_run_extra.bats
b/tools/test/integration/dry_run_extra.bats
index 105ccf4c0..5ee8a85e8 100644
--- a/tools/test/integration/dry_run_extra.bats
+++ b/tools/test/integration/dry_run_extra.bats
@@ -17,25 +17,25 @@ esac
}
@test "dry-run: plan shows Flink Agents version (review #1 — JARs are
implicit)" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --non-interactive
[ "$status" -eq 0 ]
case "$output" in *"Flink Agents version"*"0.3.0"*) ;; *) false ;; esac
}
@test "dry-run: existing FLINK_HOME — plan shows detected version, not default
(review #2)" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --non-interactive
[ "$status" -eq 0 ]
case "$output" in *"v2.1.1"*) ;; *) false ;; esac
}
@test "dry-run: INSTALL_FLINK=No suppresses Install directory line (review
#3)" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --non-interactive
[ "$status" -eq 0 ]
case "$output" in *"Install directory"*) false ;; *) ;; esac
}
@test "dry-run: Environment section is shown separately from Plan (review #5)"
{
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --dry-run
--non-interactive
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --dry-run --non-interactive
[ "$status" -eq 0 ]
case "$output" in *"Environment (read-only)"*) ;; *) false ;; esac
case "$output" in *"Installation plan"*) ;; *) false ;; esac
diff --git a/tools/test/integration/err_trap.bats
b/tools/test/integration/err_trap.bats
index bc553c53f..62e5ffb9b 100644
--- a/tools/test/integration/err_trap.bats
+++ b/tools/test/integration/err_trap.bats
@@ -13,7 +13,7 @@
mkdir -p "$tmp"
run env INSTALL_DIR="$tmp" FLINK_BASE_URL="https://dlcdn.apache.org/flink"
\
FLINK_VERSION=99.99.0 \
- bash "${BATS_TEST_DIRNAME}/../../install.sh" \
+ "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" \
--install-flink --non-interactive
[ "$status" -ne 0 ]
# Banner must name the stage by title.
@@ -31,7 +31,7 @@
# die() uses `exit`, which doesn't trigger ERR. Reach it by giving
# plan_flink a non-existent FLINK_HOME under --non-interactive.
run env FLINK_HOME="" \
- bash "${BATS_TEST_DIRNAME}/../../install.sh" --non-interactive
+ "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --non-interactive
[ "$status" -ne 0 ]
case "$output" in *"FLINK_HOME is not set"*) ;; *) false ;; esac
# Banner box characters must NOT appear in a die() path.
diff --git a/tools/test/integration/help.bats b/tools/test/integration/help.bats
index d77335e37..91f055e75 100644
--- a/tools/test/integration/help.bats
+++ b/tools/test/integration/help.bats
@@ -1,7 +1,7 @@
#!/usr/bin/env bats
@test "--help prints usage and exits 0" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" --help
[ "$status" -eq 0 ]
case "$output" in *"Apache Flink Agents Installer"*) ;; *) false ;; esac
case "$output" in *"Options:"*) ;; *) false ;; esac
@@ -10,7 +10,7 @@
}
@test "-h prints usage and exits 0" {
- run bash "${BATS_TEST_DIRNAME}/../../install.sh" -h
+ run "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" -h
[ "$status" -eq 0 ]
case "$output" in *"Apache Flink Agents Installer"*) ;; *) false ;; esac
}
diff --git a/tools/test/integration/venv_dir_validation.bats
b/tools/test/integration/venv_dir_validation.bats
index 55fc23155..11ed59c7c 100644
--- a/tools/test/integration/venv_dir_validation.bats
+++ b/tools/test/integration/venv_dir_validation.bats
@@ -26,7 +26,7 @@ esac
: > "$foreign/unrelated.txt"
run env VENV_DIR="$foreign" PYTHON_BIN=/no/such/python3 \
- bash "${BATS_TEST_DIRNAME}/../../install.sh" \
+ "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" \
--non-interactive --enable-pyflink
[ "$status" -ne 0 ]
@@ -49,7 +49,7 @@ esac
# plan_pyflink validation. Use --dry-run so plan succeeds and we
# bail before stage 3.
run env VENV_DIR="$venv" PYTHON_BIN=/no/such/python3 \
- bash "${BATS_TEST_DIRNAME}/../../install.sh" \
+ "${FLINK_AGENTS_SUT_BASH:-bash}"
"${BATS_TEST_DIRNAME}/../../install.sh" \
--non-interactive --enable-pyflink --dry-run
# Either the dry-run printout completes (status 0) or fails for an
# unrelated reason (e.g. PYTHON_BIN missing in non-interactive
resolve_python).
diff --git a/tools/test/run.sh b/tools/test/run.sh
index 24470ad3d..956e7fc84 100755
--- a/tools/test/run.sh
+++ b/tools/test/run.sh
@@ -55,14 +55,50 @@ clone_pinned() {
mkdir -p "$CACHE"
+# Interpreter used to spawn the scripts under test, kept separate from the one
+# the harness runs on. The pin below resolves the default `bash` to this shell,
+# so leaving it alone keeps the behaviour the suite has always had. Set it to
an
+# absolute path to exercise the subjects elsewhere: an absolute path is not a
+# PATH lookup, so the pin does not intercept it, and the scripts then run on
the
+# interpreter their own users have rather than on the one this suite's
+# assertions need. Each spawn site repeats the `:-bash` default so a .bats file
+# invoked through bats directly, without this script, still names one.
+#
+# A value that is set has to name something that runs as a bash. The spawn
sites
+# would otherwise carry it to a subject that cannot start, and a subject that
+# cannot start still satisfies every assertion written as "this must fail" --
+# so the suite would report a green run over nothing. Probe rather than test
for
+# execute permission: /bin/echo is executable and answers this with its own
+# arguments instead of a version.
+if [ -n "${FLINK_AGENTS_SUT_BASH+set}" ]; then
+ sut_major=""
+ if [ -n "$FLINK_AGENTS_SUT_BASH" ]; then
+ # shellcheck disable=SC2016 # the expansion belongs to the child
+ sut_major="$("$FLINK_AGENTS_SUT_BASH" \
+ -c 'printf %s "${BASH_VERSINFO[0]}"' 2>/dev/null || true)"
+ fi
+ case "$sut_major" in
+ ''|*[!0-9]*)
+ echo "ERROR: FLINK_AGENTS_SUT_BASH is set to
'${FLINK_AGENTS_SUT_BASH}'," >&2
+ echo "which did not report a bash version." >&2
+ echo "It names the interpreter the scripts under test are spawned
on, so a" >&2
+ echo "value that is empty, missing, or not a bash leaves the suite
covering" >&2
+ echo "nothing. Point it at a bash, or unset it to spawn the
scripts on the" >&2
+ echo "interpreter running this suite." >&2
+ exit 1
+ ;;
+ esac
+fi
+export FLINK_AGENTS_SUT_BASH="${FLINK_AGENTS_SUT_BASH:-bash}"
+
# Pin the interpreter bats resolves, by putting a `bash` symlink to this shell
# ahead of everything else on PATH. $BASH is the interpreter the gate above
# accepted. $CACHE is gitignored, so the symlink does not show up in git
status.
#
-# This also re-interprets the scripts under test, which start with
-# `#!/usr/bin/env bash` themselves: while the pin is in place the suite runs
-# them under this interpreter instead of the one the developer's own PATH
-# selects.
+# This also reaches the scripts under test, which start with
+# `#!/usr/bin/env bash` themselves: a spawn site naming a bare `bash` runs the
+# subject on this interpreter instead of on the one the developer's own PATH
+# selects. That reach is what FLINK_AGENTS_SUT_BASH above exists to override.
#
# Build the link under a temp name and rename it into place. Renaming within a
# directory replaces the name in one step, so a lookup running concurrently
diff --git a/tools/test/unit/edit_plan_quote.bats
b/tools/test/unit/edit_plan_quote.bats
index 27c81d97a..6b181f30c 100644
--- a/tools/test/unit/edit_plan_quote.bats
+++ b/tools/test/unit/edit_plan_quote.bats
@@ -2,15 +2,8 @@
setup() {
load '../helpers/load'
- # The bash 3.x test skips when no such interpreter is present, and sourcing
- # install.sh replaces bats' EXIT trap, which `skip` needs in order to
report.
- # That test reaches edit_plan_quote through a child interpreter, so it
wants
- # nothing loaded here. Removing the trap instead of leaving it alone does
not
- # help; the skip is swallowed either way.
- if [[ "$BATS_TEST_DESCRIPTION" != *"bash 3.x"* ]]; then
- load_install_sh
- reset_install_sh_state
- fi
+ load_install_sh
+ reset_install_sh_state
}
# Probes the usual bash locations and prints the first one whose major version
diff --git a/tools/test/unit/install_sh_trap_isolation.bats
b/tools/test/unit/install_sh_trap_isolation.bats
new file mode 100644
index 000000000..22369353a
--- /dev/null
+++ b/tools/test/unit/install_sh_trap_isolation.bats
@@ -0,0 +1,91 @@
+#!/usr/bin/env bats
+
+# install.sh installs its EXIT, INT and ERR traps at top level, so a `source`
+# installs them too. Every file whose setup() calls load_install_sh sources it,
+# and bats prints a failing or skipped result from inside its own EXIT trap: a
+# handler laid over that one makes such a result vanish from the stream
+# altogether, and an ERR handler rewrites the trace of whatever survives.
+# Sourcing install.sh with FLINK_AGENTS_INSTALL_SH_NO_RUN=1 therefore has to
+# leave the caller's handlers as it found them.
+#
+# A test cannot observe its own missing result, so both tests below drive a
+# nested bats run over a throwaway file and read that run's TAP stream.
+
+setup() {
+ # Deliberately no `load '../helpers/load'` here: sourcing install.sh into
+ # this file is the condition under test, and would suppress its own
results.
+ INSTALL_SH="${BATS_TEST_DIRNAME}/../../install.sh"
+}
+
+# Writes a bats file that sources install.sh the way load_install_sh does and
+# then fails and skips, and prints its path. It is written at run time rather
+# than committed because run.sh collects unit/ and integration/ recursively,
+# and a committed copy would be run as a real test file.
+#
+# It cannot `load '../helpers/load'`: bats resolves a helper path against the
+# directory of the file doing the loading, which here is the temp directory.
The
+# path to install.sh arrives through the environment instead.
+write_sourcing_fixture() {
+ local fixture="$BATS_TEST_TMPDIR/sourcing_fixture.bats"
+ cat > "$fixture" <<'EOF'
+#!/usr/bin/env bats
+
+setup() {
+ export FLINK_AGENTS_INSTALL_SH_NO_RUN=1
+ # shellcheck disable=SC1090
+ source "$FIXTURE_INSTALL_SH"
+}
+
+@test "fixture: a failing assertion" {
+ [[ "expected" == "actual" ]] || false
+}
+
+@test "fixture: a skip" {
+ skip "the reason reaches the stream"
+}
+EOF
+ printf '%s' "$fixture"
+}
+
+# Fails the calling test after printing the nested run's TAP stream, which
every
+# assertion below inspects and which is otherwise unrecoverable: bats removes
+# $BATS_TEST_TMPDIR with the rest of the run's temporary tree once the run
ends,
+# taking the fixture and its output with it. A test body's stderr reaches the
+# report as comment lines.
+fail_with_tap() {
+ printf 'nested TAP stream:\n%s\n' "$output" >&2
+ false
+}
+
+@test "sourcing install.sh: a failing assertion is reported, and blamed on the
test" {
+ local fixture
+ fixture="$(write_sourcing_fixture)"
+ # The line the trace has to name, read back from the fixture so it stays
+ # correct when the fixture above is edited.
+ local fail_line
+ fail_line="$(grep -n '"expected" == "actual"' "$fixture" | cut -d: -f1)"
+
+ run env FIXTURE_INSTALL_SH="$INSTALL_SH" \
+ "$BATS_ROOT/bin/bats" --formatter tap "$fixture"
+
+ [[ "$output" == *"not ok 1 fixture: a failing assertion"* ]] ||
fail_with_tap
+ # The opening of the trace, not just the file it names. A trace bats
+ # produced itself opens with `# (in test file `; install.sh's ERR handler
+ # prepends its own `from function 'on_error'` frame, displacing that
+ # opening.
+ [[ "$output" == *"# (in test file $fixture, line $fail_line)"* ]] ||
fail_with_tap
+ # A trace led by install.sh's handler, and the banner it prints, both blame
+ # the installer for a failure that belongs to the test.
+ [[ "$output" != *"on_error"* ]] || fail_with_tap
+ [[ "$output" != *"Installation failed"* ]] || fail_with_tap
+}
+
+@test "sourcing install.sh: a skip is reported" {
+ local fixture
+ fixture="$(write_sourcing_fixture)"
+
+ run env FIXTURE_INSTALL_SH="$INSTALL_SH" \
+ "$BATS_ROOT/bin/bats" --formatter tap "$fixture"
+
+ [[ "$output" == *"ok 2 fixture: a skip # skip the reason reaches the
stream"* ]] || fail_with_tap
+}
diff --git a/tools/test/unit/spawn_site_interpreter.bats
b/tools/test/unit/spawn_site_interpreter.bats
new file mode 100644
index 000000000..17fac76ca
--- /dev/null
+++ b/tools/test/unit/spawn_site_interpreter.bats
@@ -0,0 +1,126 @@
+#!/usr/bin/env bats
+
+# run.sh puts a `bash` symlink to its own interpreter at the front of PATH, so
+# every `#!/usr/bin/env bash` hop inside the run resolves to the shell whose
+# version the suite's assertions depend on. That pin reaches the scripts under
+# test as well, which is more than it was meant to do: those scripts run on
+# whatever interpreter their own users have. FLINK_AGENTS_SUT_BASH separates
the
+# two, and every site that spawns a script under test names it instead of a
bare
+# `bash`. An absolute path is not a PATH lookup, which is what lets it out from
+# under the pin.
+#
+# A test cannot observe the interpreter another test's subject ran on, so the
+# first test drives a nested bats run over a real file from the suite and reads
+# back what that run reached. The other two are static: they are what keeps a
+# site added later from quietly going back to the pinned interpreter.
+
+setup() {
+ SUITE_ROOT="$BATS_TEST_DIRNAME/.."
+ # The smallest file that spawns a script under test, and the only one that
+ # needs no helpers, so a nested run of it isolates the spawn.
+ HELP_BATS="$SUITE_ROOT/integration/help.bats"
+}
+
+# An interpreter that records having been reached and then hands its arguments
+# to the shell running this file. Standing in for a second real bash keeps the
+# assertion independent of which interpreters the machine happens to carry.
+write_recording_interpreter() {
+ local dir="$1"
+ mkdir -p "$dir"
+ cat > "$dir/bash" <<EOF
+#!/usr/bin/env bash
+printf 'reached\n' >> "$dir/reached"
+exec "$BASH" "\$@"
+EOF
+ chmod +x "$dir/bash"
+ printf '%s' "$dir/bash"
+}
+
+fail_with_tap() {
+ printf 'nested TAP stream:\n%s\n' "$output" >&2
+ false
+}
+
+@test "spawn site: a converted file spawns its subject on
FLINK_AGENTS_SUT_BASH" {
+ local dir="$BATS_TEST_TMPDIR/sut"
+ local interpreter
+ interpreter="$(write_recording_interpreter "$dir")"
+
+ # Read off the file rather than written here: a test added to help.bats
+ # later moves the expectation with it, and a spawn that stops naming the
+ # interpreter still counts, so reverting one is what the difference
reports.
+ local spawns
+ spawns="$(grep -cE '^[[:space:]]*run .*install\.sh' "$HELP_BATS")"
+
+ run env FLINK_AGENTS_SUT_BASH="$interpreter" \
+ "$BATS_ROOT/bin/bats" --formatter tap "$HELP_BATS"
+
+ [ "$status" -eq 0 ] || fail_with_tap
+ [ -f "$dir/reached" ] || fail_with_tap
+ # Every spawn arrived at the named interpreter, not just one of them.
+ [ "$(wc -l < "$dir/reached")" -eq "$spawns" ] || fail_with_tap
+}
+
+@test "spawn site: no test file spawns a script under test on the pinned
interpreter" {
+ # Two ways a spawn ends up back on the harness's shell. A bare `bash` is a
+ # PATH lookup, which the pin claims. Handing the script's own path to `run`
+ # relies on its `#!/usr/bin/env bash`, which is the same lookup one step
+ # later. Either way the subject runs on the interpreter the assertions need
+ # rather than the one its users have, and nothing reports it.
+ #
+ # Both patterns read one line at a time, so a spawn split across a line
+ # continuation is outside what they can see; recognising that needs context
+ # a line-oriented rule does not have.
+ #
+ # Two bare spawns are legitimate and are recognised by what the line names
+ # rather than by where it sits, so neither rots when a file is edited:
+ #
+ # - a line naming FLINK_AGENTS_SUT_BASH: the bare `bash -c` there owns a
+ # redirection that `run` cannot carry, and the subject on that line is
+ # spawned by the inner, named interpreter.
+ # - a line naming FLINK_AGENTS_RECOVERY_SH_NO_RUN: it sources the
+ # checkpoint-recovery script, which CI exercises on ubuntu only, so the
+ # interpreter this variable selects is not one that subject ever meets.
+ #
+ # The second pattern names the three scripts under test and the variables
+ # that hold their paths, rather than any path at all: a rule wide enough to
+ # cover every conceivable spelling would fire on the fixtures and stubs
+ # these files write, and a check that cries wolf gets deleted.
+ local bare='(^|[^-[:alnum:]_/$"])bash[[:space:]]+("?\$|-c )'
+ local direct='(^|[[:space:]])run (env ([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|[^
"]*) )+)?'
+
direct+='"(\$\{?(UT_SH|BUILD_SCRIPT|INSTALL_SH)\}?|[^"]*/(install|ut|build)\.sh)"'
+
+ local offenders
+ offenders="$( { grep -rnE "$bare" \
+ "$SUITE_ROOT/unit" "$SUITE_ROOT/integration"
--include='*.bats' \
+ | grep -v -e 'FLINK_AGENTS_SUT_BASH' -e
'FLINK_AGENTS_RECOVERY_SH_NO_RUN'
+ grep -rnE "$direct" \
+ "$SUITE_ROOT/unit" "$SUITE_ROOT/integration"
--include='*.bats'
+ } | sort -u || true)"
+ if [[ -n "$offenders" ]]; then
+ printf 'spawn sites not naming the subject interpreter:\n%s\n'
"$offenders" >&2
+ false
+ fi
+}
+
+@test "spawn site: every one falls back to the harness's \`bash\`" {
+ # The fallback has to be the bare word, so an unset variable keeps the
+ # behaviour the suite had before it existed, and it has to keep the colon,
+ # so a caller that exports the variable empty does not leave the site
+ # naming nothing. A fixed path here would move every subject off the
+ # harness silently.
+ #
+ # This file names the variable in prose and drives it in the nested run
+ # above, so it is excluded by name rather than by path: grep reports the
+ # path it walked, which is not the one BATS_TEST_FILENAME holds.
+ local wrong
+ wrong="$(grep -rn 'FLINK_AGENTS_SUT_BASH' \
+ "$SUITE_ROOT/unit" "$SUITE_ROOT/integration" --include='*.bats' \
+ | grep -v "/$(basename "$BATS_TEST_FILENAME"):" \
+ | grep -v '\${FLINK_AGENTS_SUT_BASH:-bash}' \
+ || true)"
+ if [[ -n "$wrong" ]]; then
+ printf 'spawn sites not using ${FLINK_AGENTS_SUT_BASH:-bash}:\n%s\n'
"$wrong" >&2
+ false
+ fi
+}
diff --git a/tools/test/unit/ut_sh_flink_flag_scope.bats
b/tools/test/unit/ut_sh_flink_flag_scope.bats
index fa437c2c6..67768bce0 100644
--- a/tools/test/unit/ut_sh_flink_flag_scope.bats
+++ b/tools/test/unit/ut_sh_flink_flag_scope.bats
@@ -73,12 +73,12 @@ EOF
}
@test "-f with the Java unit tests is rejected, on stderr" {
- run bash "$UT_SH" -j -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -j -f 1.20
[ "$status" -eq 1 ]
assert_rejected
# Dropping stderr must drop the message with it: an error on stdout would
# land in the middle of test output that gets parsed or piped.
- run bash -c "bash '$UT_SH' -j -f 1.20 2>/dev/null"
+ run bash -c "'${FLINK_AGENTS_SUT_BASH:-bash}' '$UT_SH' -j -f 1.20
2>/dev/null"
[ "$status" -eq 1 ]
assert_not_rejected
}
@@ -87,7 +87,7 @@ EOF
# The Python tests do install the version they are given, so this is the
# combination a scope check written around the Java suite alone would let
# through -- and outside -e it is just as inapplicable.
- run bash "$UT_SH" -p -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p -f 1.20
[ "$status" -eq 1 ]
assert_rejected
}
@@ -99,14 +99,14 @@ EOF
# three-component value describes a precision the install does not have.
local fake
fake="$(make_fake_root_pinning 9.9.9 9.9)"
- run bash "$fake/tools/ut.sh" -p -f 9.9
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" -p -f 9.9
[ "$status" -eq 1 ]
case "$(flowed_output)" in *"apache-flink~=9.9.0"*) ;; *) false ;; esac
case "$(flowed_output)" in *"9.9.9"*) false ;; *) ;; esac
}
@test "-f is accepted with the e2e tests, and still selects the version they
use" {
- run bash "$UT_SH" -j -e -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -j -e -f 1.20
[ "$status" -eq 0 ]
assert_not_rejected
# Exit 0 alone would still hold if -f had become a no-op, so pin the two
@@ -114,7 +114,7 @@ EOF
# profile the e2e run activates.
case "$(shim_calls mvn)" in *"dist/flink-1.20"*) ;; *) false ;; esac
case "$(shim_calls mvn)" in *"-Pflink-1.20"*) ;; *) false ;; esac
- run bash "$UT_SH" -p -e -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p -e -f 1.20
[ "$status" -eq 0 ]
assert_not_rejected
case "$(shim_calls uv)" in *"apache-flink~=1.20.0"*) ;; *) false ;; esac
@@ -123,20 +123,20 @@ EOF
@test "-e is honored after -f, not only before it" {
# The guard reads the parse loop's final state rather than the order the
# flags arrive in; folding it into the -f case branch would break this.
- run bash "$UT_SH" -p -f 1.20 -e
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p -f 1.20 -e
[ "$status" -eq 0 ]
assert_not_rejected
case "$(shim_calls uv)" in *"apache-flink~=1.20.0"*) ;; *) false ;; esac
}
@test "a run that passes no -f is not rejected over the defaulted version" {
- run bash "$UT_SH" -j
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -j
[ "$status" -eq 0 ]
assert_not_rejected
}
@test "an unsupported version outside -e is reported as a scope error, not an
unsupported one" {
- run bash "$UT_SH" -p -f 9.9
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p -f 9.9
[ "$status" -eq 1 ]
assert_rejected
# No value of -f applies here, so naming 9.9 as the problem would send the
@@ -149,7 +149,7 @@ EOF
# neither shim may have been reached by the time the script exits. Driven
# with no suite flag, which selects both, so a rejection reached from only
# one of the two suite paths cannot pass this.
- run bash "$UT_SH" -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -f 1.20
[ "$status" -eq 1 ]
assert_rejected
[ "$(shim_call_count mvn)" -eq 0 ]
@@ -159,10 +159,10 @@ EOF
@test "the script demonstrates no -f form it would itself reject" {
# Both places -f usage is advertised: the help text, and the error shown
# when -f is given no version, which prints the help after it.
- run bash "$UT_SH" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" --help
[ "$status" -eq 0 ]
assert_no_rejectable_f_example
- run bash -c "bash '$UT_SH' -f 2>&1"
+ run bash -c "'${FLINK_AGENTS_SUT_BASH:-bash}' '$UT_SH' -f 2>&1"
[ "$status" -eq 1 ]
assert_no_rejectable_f_example
}
diff --git a/tools/test/unit/ut_sh_flink_version_supported.bats
b/tools/test/unit/ut_sh_flink_version_supported.bats
index eba044f31..47f2cf571 100644
--- a/tools/test/unit/ut_sh_flink_version_supported.bats
+++ b/tools/test/unit/ut_sh_flink_version_supported.bats
@@ -33,7 +33,7 @@ dist_module_versions() {
@test "a Flink version with no dist module is rejected before any test runs,
on stderr" {
shim_bin mvn
shim_bin uv
- run bash "$UT_SH" -e -f 9.9
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -e -f 9.9
[ "$status" -eq 1 ]
case "$output" in *"Error: unsupported Flink version '9.9'"*) ;; *) false
;; esac
# The message has to carry the way out of the mistake, not just report it.
@@ -42,7 +42,7 @@ dist_module_versions() {
# a typo costs no build.
[ "$(shim_call_count mvn)" -eq 0 ]
[ "$(shim_call_count uv)" -eq 0 ]
- run bash -c "bash '$UT_SH' -e -f 9.9 2>/dev/null"
+ run bash -c "'${FLINK_AGENTS_SUT_BASH:-bash}' '$UT_SH' -e -f 9.9
2>/dev/null"
case "$output" in *"unsupported Flink version"*) false ;; *) ;; esac
}
@@ -50,7 +50,7 @@ dist_module_versions() {
shim_bin uv
local version
while read -r version; do
- run bash "$UT_SH" -p -e -f "$version"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p -e -f "$version"
[ "$status" -eq 0 ]
done < <(dist_versions)
}
@@ -58,7 +58,7 @@ dist_module_versions() {
@test "--help lists the dist modules that exist rather than a literal" {
local fake
fake="$(make_fake_root_pinning 3.0.0 3.0 4.1)"
- run bash "$fake/tools/ut.sh" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" --help
[ "$status" -eq 0 ]
case "$output" in *"Supported versions: 3.0 4.1"*) ;; *) false ;; esac
}
@@ -72,9 +72,9 @@ dist_module_versions() {
shim_bin uv
local fake
fake="$(make_fake_root_pinning 3.0.0 3.0 4.1)"
- run bash "$fake/tools/ut.sh" -p -e -f 4.1
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" -p -e -f 4.1
[ "$status" -eq 0 ]
- run bash "$fake/tools/ut.sh" -p -e -f 1.20
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" -p -e -f 1.20
[ "$status" -eq 1 ]
case "$output" in *"Error: unsupported Flink version '1.20'"*) ;; *) false
;; esac
case "$output" in *"supported versions: 3.0 4.1"*) ;; *) false ;; esac
@@ -89,7 +89,7 @@ dist_module_versions() {
shim_bin uv
local fake
fake="$(make_fake_root_pinning 9.9.9 3.0 4.1)"
- run bash "$fake/tools/ut.sh"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh"
[ "$status" -eq 1 ]
case "$output" in *"the root pom pins <flink.version> 9.9.9"*) ;; *) false
;; esac
case "$output" in *"carries no flink-9.9 module"*) ;; *) false ;; esac
@@ -106,7 +106,7 @@ dist_module_versions() {
shim_bin uv
local fake
fake="$(make_fake_root_pinning 2.3.0)"
- run bash "$fake/tools/ut.sh"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh"
[ "$status" -eq 1 ]
case "$output" in *"Error: found no dist/flink-* modules under"*) ;; *)
false ;; esac
case "$output" in *"carries no flink-2.3 module"*) false ;; *) ;; esac
diff --git a/tools/test/unit/ut_sh_version_default.bats
b/tools/test/unit/ut_sh_version_default.bats
index 508549d54..e4f8737c5 100644
--- a/tools/test/unit/ut_sh_version_default.bats
+++ b/tools/test/unit/ut_sh_version_default.bats
@@ -16,14 +16,14 @@ repo_flink_minor() {
}
@test "--help does not claim every Flink version is tested by default" {
- run bash "$UT_SH" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" --help
[ "$status" -eq 0 ]
case "$output" in *"all versions"*) false ;; *) ;; esac
case "$output" in *"all Flink versions"*) false ;; *) ;; esac
}
@test "--help states the default Flink version and which suites -f applies to"
{
- run bash "$UT_SH" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" --help
[ "$status" -eq 0 ]
case "$output" in *"Default: $(repo_flink_minor)"*) ;; *) false ;; esac
# The scope sentence wraps across help lines; compare on collapsed
@@ -56,7 +56,7 @@ repo_flink_minor() {
# pinning a different version separates interpolation from a literal.
local fake flowed
fake="$(make_fake_root_pinning 9.9.9 9.9)"
- run bash "$fake/tools/ut.sh" --help
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" --help
[ "$status" -eq 0 ]
flowed="$(printf '%s' "$output" | tr -s '[:space:]' ' ')"
case "$flowed" in
@@ -67,7 +67,7 @@ repo_flink_minor() {
@test "a bare Python run installs the Flink version the root pom pins" {
shim_bin uv
- run bash "$UT_SH" -p
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$UT_SH" -p
[ "$status" -eq 0 ]
case "$(shim_calls uv)" in
*"apache-flink~=$(repo_flink_minor).0"*) ;;
@@ -81,7 +81,7 @@ repo_flink_minor() {
# The fake tree carries the dist module its pom pins, so the defaulted
# version is a supported one and only the value being read is under test.
fake="$(make_fake_root ' <flink.version>9.9.9</flink.version>' 9.9)"
- run bash "$fake/tools/ut.sh" -p
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" -p
[ "$status" -eq 0 ]
case "$(shim_calls uv)" in *"apache-flink~=9.9.0"*) ;; *) false ;; esac
}
@@ -90,7 +90,7 @@ repo_flink_minor() {
shim_bin uv
local fake
fake="$(make_fake_root ' <flink.version> 9.9.9 </flink.version>' 9.9)"
- run bash "$fake/tools/ut.sh" -p
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh" -p
[ "$status" -eq 0 ]
case "$(shim_calls uv)" in *"apache-flink~=9.9.0"*) ;; *) false ;; esac
}
@@ -98,7 +98,7 @@ repo_flink_minor() {
@test "a pom carrying no flink.version at all is fatal" {
local fake
fake="$(make_fake_root ' <other.version>1.0.0</other.version>')"
- run bash "$fake/tools/ut.sh"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh"
[ "$status" -eq 1 ]
# Nothing was read, so the error must not quote a value as though one was.
case "$output" in
@@ -111,7 +111,7 @@ repo_flink_minor() {
@test "a two-component flink.version is fatal rather than a one-component
token" {
local fake
fake="$(make_fake_root ' <flink.version>2.3</flink.version>')"
- run bash "$fake/tools/ut.sh"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh"
[ "$status" -eq 1 ]
# The value was read; the error has to say so rather than claim it could
not be.
case "$output" in
@@ -124,7 +124,7 @@ repo_flink_minor() {
@test "a flink.version holding a property reference is fatal rather than a
broken token" {
local fake
fake="$(make_fake_root '
<flink.version>${flink.2.3.version}</flink.version>')"
- run bash "$fake/tools/ut.sh"
+ run "${FLINK_AGENTS_SUT_BASH:-bash}" "$fake/tools/ut.sh"
[ "$status" -eq 1 ]
# A value is present, so the error has to quote it rather than report an
# absent element the way the missing-property case does.