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 7e7dfad5 [tools][test][e2e] Make the bats suite fail on assertions
that stop holding (#1043)
7e7dfad5 is described below
commit 7e7dfad5e9b007badb8beed2f8fafe6f7ce09fb6
Author: Weiqing Yang <[email protected]>
AuthorDate: Wed Aug 26 01:27:05 2026 -0700
[tools][test][e2e] Make the bats suite fail on assertions that stop holding
(#1043)
Generated-by: Claude Code 2.1.240 (Claude Opus 5)
---
.github/workflows/ci.yml | 2 +-
.../test-scripts/test_submit_examples_to_flink.sh | 6 +-
tools/install.sh | 13 ++++-
tools/test/helpers/bash_version_guard.bash | 23 ++++++++
tools/test/integration/build_help.bats | 16 ++---
tools/test/run.sh | 53 ++++++++++++++---
tools/test/unit/check_license.bats | 20 +++----
tools/test/unit/checkpoint_recovery_harness.bats | 68 +++++++++++-----------
tools/test/unit/edit_plan_quote.bats | 53 ++++++++++++++++-
tools/test/unit/parse_args.bats | 4 +-
tools/test/unit/platform_detect.bats | 2 +-
tools/test/unit/revalidate_python_constraint.bats | 8 +--
tools/test/unit/shim_self_test.bats | 2 +-
tools/test/unit/ui_helpers.bats | 18 +++---
tools/test/unit/verify_example_job.bats | 30 +++++-----
15 files changed, 221 insertions(+), 97 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 89df9c85..9a67c4eb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,7 +62,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- - name: Install bash 4+ (macOS)
+ - name: Install bash 4.1+ (macOS)
if: runner.os == 'macOS'
run: brew install bash
- name: Run tools script tests
diff --git a/e2e-test/test-scripts/test_submit_examples_to_flink.sh
b/e2e-test/test-scripts/test_submit_examples_to_flink.sh
index 7b479c42..97981449 100755
--- a/e2e-test/test-scripts/test_submit_examples_to_flink.sh
+++ b/e2e-test/test-scripts/test_submit_examples_to_flink.sh
@@ -359,7 +359,11 @@ remove_submission_pid() {
remaining_pids+=("$pid")
fi
done
- SUBMISSION_PIDS=("${remaining_pids[@]}")
+ # Rebuild with the `+` form: on bash 4.3 and older "${arr[@]}" on an empty
+ # array is an unbound-variable error under `set -u`, and "${arr[@]:-}"
would
+ # rebuild one empty-string element instead of nothing. The `:-` in the loop
+ # above is correct: its empty iteration is dropped by the `-n` guard.
+ SUBMISSION_PIDS=("${remaining_pids[@]+"${remaining_pids[@]}"}")
}
start_attached_java_submission() {
diff --git a/tools/install.sh b/tools/install.sh
index 68963ca3..3f9033be 100755
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -1022,9 +1022,20 @@ reconcile_plan_after_edit() {
# Quote a value for safe re-sourcing by the parent shell via single-quoted
# assignment. Any embedded single quotes are escaped by closing/reopening
# the quoted string.
+#
+# Implementation note: the escape is supplied through a variable rather than
+# written inline. Written inline as ${v//\'/\'\\\'\'}, bash 3.2 (macOS
+# /bin/bash) emits 'it\'\\'\'s' where the intended output is 'it'\''s'. The
+# 3.2 form is not valid shell, so sourcing the dumped state file fails with
+# "unexpected EOF while looking for matching `'" and the value is lost.
+# Held in escaped_quote the escape reaches the output untouched: 3.2.57 and
+# 5.3.15 emit identical bytes. Keep escaped_quote at a single backslash;
+# with two, they stop matching.
+# tools/test/unit/edit_plan_quote.bats pins this under a real bash 3.x.
edit_plan_quote() {
local v="$1"
- printf "'%s'" "${v//\'/\'\\\'\'}"
+ local escaped_quote="'\\''"
+ printf "'%s'" "${v//\'/$escaped_quote}"
}
# Write the subset of plan variables we may have modified to a sourceable
diff --git a/tools/test/helpers/bash_version_guard.bash
b/tools/test/helpers/bash_version_guard.bash
new file mode 100644
index 00000000..e2734f1b
--- /dev/null
+++ b/tools/test/helpers/bash_version_guard.bash
@@ -0,0 +1,23 @@
+# Suite-level assertion that the interpreter running the test bodies is bash
+# 4.1 or newer. Wired in explicitly by run.sh via --setup-suite-file, because
+# auto-discovery probes the directory named by each path argument and would
+# resolve to unit/ instead.
+#
+# This backs up the PATH pin in run.sh rather than replacing it: bats starts
+# each test process through `#!/usr/bin/env bash`, so if the pin ever stops
+# resolving, the run would otherwise continue under whatever bash PATH finds.
+#
+# A failing setup_suite aborts the run before any test body executes. Use
+# `return 1`, not `exit 1`, which bats reports as "`exit 1' failed with
+# status 0".
+
+setup_suite() {
+ if (( 10 * BASH_VERSINFO[0] + BASH_VERSINFO[1] < 41 )); then
+ echo "ERROR: bats is running the test bodies under bash
${BASH_VERSION}," >&2
+ echo "but this suite requires bash >= 4.1." >&2
+ echo "Start the suite through tools/test/run.sh, which pins the
interpreter" >&2
+ echo "for the whole run. If it did start there, the pin is not
resolving:" >&2
+ echo "inspect tools/test/.bats-cache/shim/bash." >&2
+ return 1
+ fi
+}
diff --git a/tools/test/integration/build_help.bats
b/tools/test/integration/build_help.bats
index d4a4abeb..5fc5ccfd 100644
--- a/tools/test/integration/build_help.bats
+++ b/tools/test/integration/build_help.bats
@@ -24,24 +24,24 @@ BUILD_SCRIPT="${BATS_TEST_DIRNAME}/../../build.sh"
run bash "$BUILD_SCRIPT" --help
[ "$status" -eq 0 ]
- [[ "$output" == *"Build Flink Agents Java and Python artifacts"* ]]
- [[ "$output" == *"Usage:"* ]]
- [[ "$output" == *"--java"* ]]
- [[ "$output" == *"--python"* ]]
+ [[ "$output" == *"Build Flink Agents Java and Python artifacts"* ]] ||
false
+ [[ "$output" == *"Usage:"* ]] || false
+ [[ "$output" == *"--java"* ]] || false
+ [[ "$output" == *"--python"* ]] || false
}
@test "build -h prints usage and exits 0" {
run bash "$BUILD_SCRIPT" -h
[ "$status" -eq 0 ]
- [[ "$output" == *"Usage:"* ]]
+ [[ "$output" == *"Usage:"* ]] || false
}
@test "build rejects an unknown option with usage" {
run bash "$BUILD_SCRIPT" --no-such-option
[ "$status" -eq 1 ]
- [[ "$output" == *"Error: Unknown option '--no-such-option'"* ]]
- [[ "$output" == *"Usage:"* ]]
- [[ "$output" != *"show_help: command not found"* ]]
+ [[ "$output" == *"Error: Unknown option '--no-such-option'"* ]] || false
+ [[ "$output" == *"Usage:"* ]] || false
+ [[ "$output" != *"show_help: command not found"* ]] || false
}
diff --git a/tools/test/run.sh b/tools/test/run.sh
index c5d303d7..24470ad3 100755
--- a/tools/test/run.sh
+++ b/tools/test/run.sh
@@ -17,13 +17,23 @@
# limitations under the License.
################################################################################
-# Bash 4+ is required: bash 3.2 (macOS default) does not trigger `set -e`
-# on `[[ ]]` failures or fire the ERR trap on them, which means many
-# substring assertions in this suite would silently pass on bash 3.2.
-# Force a clean failure here rather than mislead developers.
-if [ -z "${BASH_VERSION:-}" ] || [ "${BASH_VERSION%%.*}" -lt 4 ]; then
- echo "ERROR: bash >= 4 required (detected: ${BASH_VERSION:-unknown})." >&2
- echo "macOS ships bash 3.2 at /bin/bash; install bash 4+ via Homebrew:" >&2
+# Bash 4.1 or newer is required: on older bash, `set -e` does not trigger on a
+# failing `[[ ]]` and the ERR trap does not fire for it, so many of the
substring
+# assertions in this suite would silently pass. That changed in 4.1, so 4.0 is
+# rejected as well; macOS ships 3.2 at /bin/bash. Force a clean failure here
+# rather than mislead developers.
+#
+# The gate below binds this shell only. bats evaluates each test body in a
+# separate process started through `#!/usr/bin/env bash`, so the interpreter is
+# re-resolved from PATH at every hop. Two further steps cover that: the PATH
pin
+# below points `bash` at the interpreter this gate accepted, and
+# helpers/bash_version_guard.bash re-checks the version from inside the run.
+#
+# The empty-BASH_VERSION test must stay first: it short-circuits, so the
+# arithmetic is never evaluated under a shell that has no BASH_VERSINFO.
+if [ -z "${BASH_VERSION:-}" ] || (( 10 * BASH_VERSINFO[0] + BASH_VERSINFO[1] <
41 )); then
+ echo "ERROR: bash >= 4.1 required (detected: ${BASH_VERSION:-unknown})."
>&2
+ echo "macOS ships bash 3.2 at /bin/bash; install bash 4.1+ via Homebrew:"
>&2
echo " brew install bash" >&2
echo "Then run with the new bash, e.g.:" >&2
echo " /opt/homebrew/bin/bash $0" >&2
@@ -44,10 +54,37 @@ clone_pinned() {
}
mkdir -p "$CACHE"
+
+# 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.
+#
+# 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
+# sees either the old link or the new one; `ln -sfn` instead unlinks before it
+# re-creates, leaving a window in which the name does not exist and the lookup
+# falls through to the next PATH entry.
+SHIM="$CACHE/shim"
+mkdir -p "$SHIM"
+# A directory here would swallow the rename instead of being replaced by it.
+if [[ -d "$SHIM/bash" ]]; then rm -rf "$SHIM/bash"; fi
+rm -f "$SHIM/bash.$$"
+ln -s "$BASH" "$SHIM/bash.$$"
+mv -f "$SHIM/bash.$$" "$SHIM/bash"
+PATH="$SHIM:$PATH"
+export PATH
+
clone_pinned bats-core https://github.com/bats-core/bats-core.git v1.11.0
clone_pinned bats-support https://github.com/bats-core/bats-support.git v0.3.0
clone_pinned bats-assert https://github.com/bats-core/bats-assert.git v2.1.0
export BATS_LIB_PATH="$CACHE"
-exec "$CACHE/bats-core/bin/bats" --recursive "$HERE/unit" "$HERE/integration"
+exec "$CACHE/bats-core/bin/bats" \
+ --setup-suite-file "$HERE/helpers/bash_version_guard.bash" \
+ --recursive "$HERE/unit" "$HERE/integration"
diff --git a/tools/test/unit/check_license.bats
b/tools/test/unit/check_license.bats
index d4691988..f501557c 100644
--- a/tools/test/unit/check_license.bats
+++ b/tools/test/unit/check_license.bats
@@ -40,7 +40,7 @@ setup() {
run acquire_rat_jar
[ "$status" -eq 0 ]
- [[ "$output" == *"Warning: cannot validate cached Apache RAT JAR"* ]]
+ [[ "$output" == *"Warning: cannot validate cached Apache RAT JAR"* ]] ||
false
[ -f "$rat_jar" ]
}
@@ -52,7 +52,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"is invalid"* ]]
+ [[ "$output" == *"is invalid"* ]] || false
[ ! -f "$rat_jar" ]
}
@@ -64,7 +64,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"is invalid"* ]]
+ [[ "$output" == *"is invalid"* ]] || false
[ ! -f "$rat_jar" ]
}
@@ -86,7 +86,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"Failed to download Apache RAT"* ]]
+ [[ "$output" == *"Failed to download Apache RAT"* ]] || false
[ ! -e "${rat_jar}.part" ]
[ ! -e "$rat_jar" ]
}
@@ -100,9 +100,9 @@ setup() {
[ -f "$rat_jar" ]
run cat "$SHIM_CALLS/curl.log"
- [[ "$output" == *"--fail"* ]]
- [[ "$output" == *"--show-error"* ]]
- [[ "$output" == *"--location"* ]]
+ [[ "$output" == *"--fail"* ]] || false
+ [[ "$output" == *"--show-error"* ]] || false
+ [[ "$output" == *"--location"* ]] || false
[ "$(shim_call_count unzip)" = "1" ]
}
@@ -114,7 +114,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"Cannot validate the downloaded Apache RAT JAR"* ]]
+ [[ "$output" == *"Cannot validate the downloaded Apache RAT JAR"* ]] ||
false
[ ! -e "$rat_jar" ]
[ ! -e "${rat_jar}.part" ]
}
@@ -139,7 +139,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"is invalid"* ]]
+ [[ "$output" == *"is invalid"* ]] || false
[ ! -f "$rat_jar" ]
}
@@ -151,7 +151,7 @@ setup() {
run acquire_rat_jar
[ "$status" -ne 0 ]
- [[ "$output" == *"is invalid"* ]]
+ [[ "$output" == *"is invalid"* ]] || false
[ ! -f "$rat_jar" ]
}
diff --git a/tools/test/unit/checkpoint_recovery_harness.bats
b/tools/test/unit/checkpoint_recovery_harness.bats
index a9a733c0..760696c7 100644
--- a/tools/test/unit/checkpoint_recovery_harness.bats
+++ b/tools/test/unit/checkpoint_recovery_harness.bats
@@ -250,8 +250,8 @@ setup() {
REST_STUB_BODY=""
run wait_for_rest "stub" "/stub" "int:counts.total" "int:counts.completed"
"ge:99" 3
[ "$status" -eq 1 ]
- [[ "$output" == *"never parsed"* ]]
- [[ "$output" == *"NOT evidence"* ]]
+ [[ "$output" == *"never parsed"* ]] || false
+ [[ "$output" == *"NOT evidence"* ]] || false
}
@test "wait_for_rest: probe readable but target not blames the field name" {
@@ -259,9 +259,9 @@ setup() {
REST_STUB_BODY="$(fixture_checkpoints)"
run wait_for_rest "stub" "/stub" "int:counts.total"
"int:counts.completedx" "ge:1" 3
[ "$status" -eq 1 ]
- [[ "$output" == *"so the endpoint is right"* ]]
- [[ "$output" == *"never parsed 'counts.completedx'"* ]]
- [[ "$output" == *"NOT evidence"* ]]
+ [[ "$output" == *"so the endpoint is right"* ]] || false
+ [[ "$output" == *"never parsed 'counts.completedx'"* ]] || false
+ [[ "$output" == *"NOT evidence"* ]] || false
}
@test "wait_for_rest: a genuinely false condition reports the observed value" {
@@ -269,10 +269,10 @@ setup() {
REST_STUB_BODY="$(fixture_checkpoints)"
run wait_for_rest "stub" "/stub" "int:counts.total" "int:counts.completed"
"ge:99" 3
[ "$status" -eq 1 ]
- [[ "$output" == *"was last '6'"* ]]
+ [[ "$output" == *"was last '6'"* ]] || false
# A condition that really was evaluated and really was false must not carry
# the disclaimer, or the disclaimer stops meaning anything.
- [[ "$output" != *"NOT evidence"* ]]
+ [[ "$output" != *"NOT evidence"* ]] || false
}
@test "wait_for_rest: target observed but probe missing still reports the
value" {
@@ -283,10 +283,10 @@ setup() {
REST_STUB_BODY='{"counts":{"completed":6}}'
run wait_for_rest "stub" "/stub" "int:counts.total" "int:counts.completed"
"ge:99" 3
[ "$status" -eq 1 ]
- [[ "$output" == *"was last '6'"* ]]
- [[ "$output" != *"NOT evidence"* ]]
+ [[ "$output" == *"was last '6'"* ]] || false
+ [[ "$output" != *"NOT evidence"* ]] || false
# ...while still saying the response was not fully as expected.
- [[ "$output" == *"only partly as expected"* ]]
+ [[ "$output" == *"only partly as expected"* ]] || false
}
@test "wait_for_rest: an uncomparable value stops the wait instead of
spinning" {
@@ -294,7 +294,7 @@ setup() {
REST_STUB_BODY="$(fixture_job_running)"
run wait_for_rest "stub" "/stub" "str:state" "str:state" "ge:3" 6
[ "$status" -eq 1 ]
- [[ "$output" == *"cannot be evaluated"* ]]
+ [[ "$output" == *"cannot be evaluated"* ]] || false
}
@test "wait_for_rest: a slow endpoint does not multiply the budget" {
@@ -331,16 +331,16 @@ setup() {
@test "wait_for_file: an absent file times out without inventing a twin" {
run wait_for_file "stub" "$BATS_TEST_TMPDIR/verdict.json" 2
[ "$status" -eq 1 ]
- [[ "$output" == *"did not appear"* ]]
- [[ "$output" != *"leftover"* ]]
+ [[ "$output" == *"did not appear"* ]] || false
+ [[ "$output" != *"leftover"* ]] || false
}
@test "wait_for_file: a leftover .tmp twin is reported and never accepted" {
: > "$BATS_TEST_TMPDIR/verdict.json.tmp"
run wait_for_file "stub" "$BATS_TEST_TMPDIR/verdict.json" 2
[ "$status" -eq 1 ]
- [[ "$output" == *"leftover"* ]]
- [[ "$output" == *"verdict.json.tmp"* ]]
+ [[ "$output" == *"leftover"* ]] || false
+ [[ "$output" == *"verdict.json.tmp"* ]] || false
}
# ---------------------------------------------------------------------------
@@ -363,7 +363,7 @@ setup() {
run prepare_python_venv
[ "$status" -eq 0 ]
- [[ "$output" == *"Reusing Python venv"* ]]
+ [[ "$output" == *"Reusing Python venv"* ]] || false
}
@test "prepare_python_venv: a non-empty directory that is not a venv is
refused" {
@@ -374,7 +374,7 @@ setup() {
run prepare_python_venv
[ "$status" -eq 1 ]
- [[ "$output" == *"not an empty directory or a valid venv"* ]]
+ [[ "$output" == *"not an empty directory or a valid venv"* ]] || false
}
@test "prepare_python_venv: creating one targets VENV_DIR with the configured
interpreter" {
@@ -398,7 +398,7 @@ EOF
run install_built_python_package
[ "$status" -eq 1 ]
- [[ "$output" == *"Python wheel not found"* ]]
+ [[ "$output" == *"Python wheel not found"* ]] || false
}
# A jar carrying a different version has a different filename, so copying the
@@ -429,7 +429,7 @@ EOF
# The `|| false` after each [[ ]] is load-bearing, not decoration. errexit in
bash
# 3.2, the interpreter macOS supplies and bats takes test bodies from, does not
# apply to [[ ]], so a bare [[ ]] assertion cannot fail a test there — it only
-# fails from bash 4 on. Chaining a simple command onto it restores the check.
+# fails from bash 4.1 on. Chaining a simple command onto it restores the check.
# ---------------------------------------------------------------------------
@test "prepare_work_dirs: writes a trigger file the source will read" {
@@ -695,7 +695,7 @@ file_mode() {
REST_STUB_BODY="$(fixture_jobmanager_config | sed
'/restart-strategy.type/d')"
run assert_effective_config
[ "$status" -eq 1 ]
- [[ "$output" == *"absent from the cluster configuration"* ]]
+ [[ "$output" == *"absent from the cluster configuration"* ]] || false
# Recorded, not just reported: the recorded FAIL is what makes the script
# exit non-zero. `run` uses a subshell, so assert again in this one.
@@ -709,7 +709,7 @@ file_mode() {
REST_STUB_BODY="$(fixture_jobmanager_config | sed
's/"fixed-delay"/"disable"/')"
run assert_effective_config
[ "$status" -eq 1 ]
- [[ "$output" == *"restart-strategy.type is 'disable', expected
'fixed-delay'"* ]]
+ [[ "$output" == *"restart-strategy.type is 'disable', expected
'fixed-delay'"* ]] || false
}
@test "assert_effective_config: an unreachable endpoint fails" {
@@ -717,7 +717,7 @@ file_mode() {
REST_STUB_BODY=""
run assert_effective_config
[ "$status" -eq 1 ]
- [[ "$output" == *"Could not read"* ]]
+ [[ "$output" == *"Could not read"* ]] || false
}
# ---------------------------------------------------------------------------
@@ -739,7 +739,7 @@ file_mode() {
REST_STUB_BODY=""
run assert_checkpointing_enabled
[ "$status" -eq 1 ]
- [[ "$output" == *"checkpointing is off"* ]]
+ [[ "$output" == *"checkpointing is off"* ]] || false
}
@test "assert_checkpointing_enabled: a different interval fails" {
@@ -747,7 +747,7 @@ file_mode() {
REST_STUB_BODY="$(fixture_checkpoint_config | sed
's/"interval":5000/"interval":180000/')"
run assert_checkpointing_enabled
[ "$status" -eq 1 ]
- [[ "$output" == *"180000ms, expected 5000ms"* ]]
+ [[ "$output" == *"180000ms, expected 5000ms"* ]] || false
}
@test "assert_checkpointing_enabled: overlapping checkpoints warn about the
gate" {
@@ -755,7 +755,7 @@ file_mode() {
REST_STUB_BODY="$(fixture_checkpoint_config | sed
's/"max_concurrent":1/"max_concurrent":3/')"
run assert_checkpointing_enabled
[ "$status" -eq 0 ]
- [[ "$output" == *"weaker than intended"* ]]
+ [[ "$output" == *"weaker than intended"* ]] || false
}
# ---------------------------------------------------------------------------
@@ -781,7 +781,7 @@ verdict_setup() {
verdict_setup
run assert_verdict
[ "$status" -eq 1 ]
- [[ "$output" == *"nothing was verified"* ]]
+ [[ "$output" == *"nothing was verified"* ]] || false
}
@test "assert_verdict: a fail verdict fails and is recorded" {
@@ -789,7 +789,7 @@ verdict_setup() {
printf '%s' '{"verdict":"fail","blob_observed_type":"NoneType"}' >
"$VERDICT_DIR/verdict.json"
run assert_verdict
[ "$status" -eq 1 ]
- [[ "$output" == *"the job reported 'fail'"* ]]
+ [[ "$output" == *"the job reported 'fail'"* ]] || false
assert_verdict || true
[ "${RESULT_STATES[0]}" = "FAIL" ]
@@ -810,7 +810,7 @@ verdict_setup() {
printf '%s' '{"restored_blob":true}' > "$VERDICT_DIR/verdict.json"
run assert_verdict
[ "$status" -eq 1 ]
- [[ "$output" == *"disagree about the verdict format"* ]]
+ [[ "$output" == *"disagree about the verdict format"* ]] || false
}
@test "assert_verdict: an unparsable verdict file fails" {
@@ -825,7 +825,7 @@ verdict_setup() {
printf '%s' '{"verdict":"pass"}' > "$VERDICT_DIR/verdict.json.tmp"
run assert_verdict
[ "$status" -eq 1 ]
- [[ "$output" == *"leftover"* ]]
+ [[ "$output" == *"leftover"* ]] || false
}
@test "assert_verdict: a job that died without a verdict fails fast" {
@@ -859,7 +859,7 @@ payload_budget_setup() {
run assert_handshake_budget
[ "$status" -eq 0 ]
# The deadline is read from the payload module rather than restated here.
- [[ "$output" == *"240s"* ]]
+ [[ "$output" == *"240s"* ]] || false
}
@test "assert_handshake_budget: a budget that fits nominally but not in wall
clock is rejected" {
@@ -872,8 +872,8 @@ payload_budget_setup() {
CHECKPOINT_TIMEOUT=45 TM_GONE_TIMEOUT=60 TM_UP_TIMEOUT=45
RESTORE_TIMEOUT=45
run assert_handshake_budget
[ "$status" -eq 1 ]
- [[ "$output" == *"could take up to"* ]]
- [[ "$output" == *"exceeds"* ]]
+ [[ "$output" == *"could take up to"* ]] || false
+ [[ "$output" == *"exceeds"* ]] || false
}
@test "assert_handshake_budget: a grossly inflated budget is rejected" {
@@ -919,7 +919,7 @@ payload_budget_setup() {
HANDSHAKE_DEADLINE_AT=$((SECONDS - 1))
run charged_timeout "step" 30
[ "$status" -eq 1 ]
- [[ "$output" == *"elapsed before this step began"* ]]
+ [[ "$output" == *"elapsed before this step began"* ]] || false
}
@test "handshake_budget_left: never reports a negative remainder" {
@@ -999,7 +999,7 @@ run_cleanup_with_exit() { # $1 = exit code, $2 = recorded
state ("" for none),
RESULT_STATES=()
run print_summary
[ "$status" -eq 1 ]
- [[ "$output" == *"nothing was verified"* ]]
+ [[ "$output" == *"nothing was verified"* ]] || false
}
@test "print_summary: any recorded FAIL exits non-zero" {
diff --git a/tools/test/unit/edit_plan_quote.bats
b/tools/test/unit/edit_plan_quote.bats
index ac6ae60b..27c81d97 100644
--- a/tools/test/unit/edit_plan_quote.bats
+++ b/tools/test/unit/edit_plan_quote.bats
@@ -2,8 +2,29 @@
setup() {
load '../helpers/load'
- load_install_sh
- reset_install_sh_state
+ # 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
+}
+
+# Probes the usual bash locations and prints the first one whose major version
+# is 3, or nothing when none of them qualifies.
+find_bash3() {
+ local candidate
+ for candidate in /bin/bash /usr/bin/bash /usr/local/bin/bash
/opt/homebrew/bin/bash; do
+ [[ -x "$candidate" ]] || continue
+ if [[ "$("$candidate" -c 'printf %s "${BASH_VERSINFO[0]}"'
2>/dev/null)" == "3" ]]; then
+ printf '%s' "$candidate"
+ return 0
+ fi
+ done
+ return 0
}
# edit_plan_quote single-quotes its argument so the parent shell can
@@ -43,6 +64,34 @@ setup() {
[ "$out" = "$input" ]
}
+# The inline form this replaced is correct on 5.3.15 and broken on 3.2.57, so
+# a body that calls edit_plan_quote directly only catches the bug when it
+# happens to be running the older one. Invoking a real 3.x explicitly is what
+# covers the bash that install.sh's own users run.
+#
+# The value is adversarial on three axes at once. The adjacent pair of quotes
+# defeats an escape that replaces only the first occurrence. The lone quote
+# further along defeats an escape that handles only adjacent pairs. The $x
+# defeats wrapping the value in double quotes instead of single.
+@test "edit_plan_quote: repeated single quotes are escaped identically on bash
3.x" {
+ local bash3
+ bash3="$(find_bash3)"
+ [[ -n "$bash3" ]] || skip "no bash 3.x interpreter found; install one to
cover macOS /bin/bash"
+
+ local input="/tmp/o''brien's dir \$x"
+ local quoted
+ quoted="$(FLINK_AGENTS_INSTALL_SH_NO_RUN=1 "$bash3" -c \
+ '. "$1"; edit_plan_quote "$2"' _ \
+ "${BATS_TEST_DIRNAME}/../../install.sh" "$input")"
+
+ # Expected bytes: '/tmp/o'\'''\''brien'\''s dir $x'
+ [ "$quoted" = "'/tmp/o'\\'''\\''brien'\\''s dir \$x'" ]
+
+ local out
+ eval "out=$quoted"
+ [ "$out" = "$input" ]
+}
+
@test "edit_plan_quote: shell metacharacters are not expanded on source-back" {
local input='/tmp/$HOME-or-$(rm -rf /)'
local quoted
diff --git a/tools/test/unit/parse_args.bats b/tools/test/unit/parse_args.bats
index 62b6b252..70f987af 100644
--- a/tools/test/unit/parse_args.bats
+++ b/tools/test/unit/parse_args.bats
@@ -49,7 +49,7 @@ setup() {
@test "parse_args: --python without arg dies" {
run parse_args --python
[ "$status" -ne 0 ]
- [[ "$output" == *"--python requires a path argument"* ]]
+ [[ "$output" == *"--python requires a path argument"* ]] || false
}
@test "parse_args: --help sets HELP=1" {
@@ -65,7 +65,7 @@ setup() {
@test "parse_args: unknown flag warns but does not die" {
run parse_args --no-such-flag
[ "$status" -eq 0 ]
- [[ "$output" == *"Unknown option: --no-such-flag"* ]]
+ [[ "$output" == *"Unknown option: --no-such-flag"* ]] || false
}
@test "parse_args: combined flags all apply" {
diff --git a/tools/test/unit/platform_detect.bats
b/tools/test/unit/platform_detect.bats
index c1340080..b9aef8be 100644
--- a/tools/test/unit/platform_detect.bats
+++ b/tools/test/unit/platform_detect.bats
@@ -94,5 +94,5 @@ esac
unset WSL_DISTRO_NAME
run detect_os_or_die
[ "$status" -ne 0 ]
- [[ "$output" == *"Unsupported operating system"* ]]
+ [[ "$output" == *"Unsupported operating system"* ]] || false
}
diff --git a/tools/test/unit/revalidate_python_constraint.bats
b/tools/test/unit/revalidate_python_constraint.bats
index 96bbfa0a..c0580518 100644
--- a/tools/test/unit/revalidate_python_constraint.bats
+++ b/tools/test/unit/revalidate_python_constraint.bats
@@ -98,8 +98,8 @@ EOF
run revalidate_python_constraint
[ "$status" -eq 0 ]
- [[ "$output" == *"incompatible with Flink Agents 0.2.1"* ]]
- [[ "$output" == *"<3.12"* ]]
+ [[ "$output" == *"incompatible with Flink Agents 0.2.1"* ]] || false
+ [[ "$output" == *"<3.12"* ]] || false
}
# --- PYTHON_BIN re-resolution (Flink axis) ---
@@ -148,8 +148,8 @@ EOF
run revalidate_existing_venv
[ "$status" -ne 0 ]
- [[ "$output" == *"uses Python 3.12"* ]]
- [[ "$output" == *"different path"* ]]
+ [[ "$output" == *"uses Python 3.12"* ]] || false
+ [[ "$output" == *"different path"* ]] || false
}
@test "revalidate_existing_venv: no-op when the existing venv interpreter is
compatible" {
diff --git a/tools/test/unit/shim_self_test.bats
b/tools/test/unit/shim_self_test.bats
index 04def76a..a8695ac5 100644
--- a/tools/test/unit/shim_self_test.bats
+++ b/tools/test/unit/shim_self_test.bats
@@ -34,7 +34,7 @@ setup() {
@test "PATH shim is preferred over real binary" {
shim_bin curl
run command -v curl
- [[ "$output" == "$BATS_TEST_TMPDIR/bin/curl" ]]
+ [[ "$output" == "$BATS_TEST_TMPDIR/bin/curl" ]] || false
}
@test "shim_bin_missing makes command -v report missing" {
diff --git a/tools/test/unit/ui_helpers.bats b/tools/test/unit/ui_helpers.bats
index 61ad31d0..6032993c 100644
--- a/tools/test/unit/ui_helpers.bats
+++ b/tools/test/unit/ui_helpers.bats
@@ -11,44 +11,44 @@ setup() {
@test "ui_info: prints the message in fallback branch" {
run ui_info "hello world"
[ "$status" -eq 0 ]
- [[ "$output" == *"hello world"* ]]
+ [[ "$output" == *"hello world"* ]] || false
}
@test "ui_warn: prints the message in fallback branch" {
run ui_warn "be careful"
[ "$status" -eq 0 ]
- [[ "$output" == *"be careful"* ]]
+ [[ "$output" == *"be careful"* ]] || false
}
@test "ui_success: prints the message in fallback branch" {
run ui_success "all good"
[ "$status" -eq 0 ]
- [[ "$output" == *"all good"* ]]
+ [[ "$output" == *"all good"* ]] || false
}
@test "ui_error: prints the message in fallback branch" {
run ui_error "uh oh"
[ "$status" -eq 0 ]
- [[ "$output" == *"uh oh"* ]]
+ [[ "$output" == *"uh oh"* ]] || false
}
@test "ui_kv: prints key and value" {
run ui_kv "Flink version" "2.2.0"
[ "$status" -eq 0 ]
- [[ "$output" == *"Flink version"* ]]
- [[ "$output" == *"2.2.0"* ]]
+ [[ "$output" == *"Flink version"* ]] || false
+ [[ "$output" == *"2.2.0"* ]] || false
}
@test "ui_stage: increments stage counter and prints title" {
INSTALL_STAGE_CURRENT=0
run ui_stage "First thing"
[ "$status" -eq 0 ]
- [[ "$output" == *"First thing"* ]]
- [[ "$output" == *"[1/${INSTALL_STAGE_TOTAL}]"* ]]
+ [[ "$output" == *"First thing"* ]] || false
+ [[ "$output" == *"[1/${INSTALL_STAGE_TOTAL}]"* ]] || false
}
@test "die: prints message and exits non-zero" {
run die "fatal boom"
[ "$status" -ne 0 ]
- [[ "$output" == *"fatal boom"* ]]
+ [[ "$output" == *"fatal boom"* ]] || false
}
diff --git a/tools/test/unit/verify_example_job.bats
b/tools/test/unit/verify_example_job.bats
index 2ed65a77..07f530d6 100644
--- a/tools/test/unit/verify_example_job.bats
+++ b/tools/test/unit/verify_example_job.bats
@@ -109,7 +109,7 @@ create_fake_executable() {
run wait_for_job_healthy "job-id" "example" 5 2
[ "$status" -eq 0 ]
- [[ "$output" == *"reached FINISHED status"* ]]
+ [[ "$output" == *"reached FINISHED status"* ]] || false
}
@test "job health check accepts a continuously RUNNING job" {
@@ -118,7 +118,7 @@ create_fake_executable() {
run wait_for_job_healthy "job-id" "example" 5 2
[ "$status" -eq 0 ]
- [[ "$output" == *"remained RUNNING for 2s"* ]]
+ [[ "$output" == *"remained RUNNING for 2s"* ]] || false
}
@test "job health check rejects a failing job" {
@@ -130,7 +130,7 @@ create_fake_executable() {
run wait_for_job_healthy "job-id" "example" 5 2
[ "$status" -ne 0 ]
- [[ "$output" == *"entered unexpected state: FAILING"* ]]
+ [[ "$output" == *"entered unexpected state: FAILING"* ]] || false
}
@test "job health check resets the stability period after a restart" {
@@ -139,7 +139,7 @@ create_fake_executable() {
run wait_for_job_healthy "job-id" "example" 4 2
[ "$status" -ne 0 ]
- [[ "$output" == *"did not become stably RUNNING or FINISHED"* ]]
+ [[ "$output" == *"did not become stably RUNNING or FINISHED"* ]] || false
}
@test "submitted job is marked failed when health verification fails" {
@@ -213,7 +213,7 @@ create_fake_executable() {
run cancel_job_after_check "job-id" "example"
[ "$status" -eq 0 ]
- [[ "$output" == *"no longer occupies the cluster slot"* ]]
+ [[ "$output" == *"no longer occupies the cluster slot"* ]] || false
}
@test "slow slot cleanup restarts the standalone cluster" {
@@ -247,13 +247,13 @@ create_fake_executable() {
run cancel_job_after_check "job-id" "example"
[ "$status" -ne 0 ]
- [[ "$output" == *"failed to cancel job job-id"* ]]
+ [[ "$output" == *"failed to cancel job job-id"* ]] || false
}
@test "CI chat model aliases cover every hardcoded quickstart model" {
- [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3:1.7b "* ]]
- [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3:8b "* ]]
- [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3.5:9b "* ]]
+ [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3:1.7b "* ]] || false
+ [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3:8b "* ]] || false
+ [[ " ${OLLAMA_CHAT_MODEL_ALIASES[*]} " == *" qwen3.5:9b "* ]] || false
}
@test "Java submission keeps the attached client alive during validation" {
@@ -310,7 +310,7 @@ create_fake_executable() {
run main
[ "$status" -ne 0 ]
- [[ "$output" == *"Java example discovery failed"* ]]
+ [[ "$output" == *"Java example discovery failed"* ]] || false
}
@test "main propagates Python quickstart discovery failure" {
@@ -322,7 +322,7 @@ create_fake_executable() {
run main
[ "$status" -ne 0 ]
- [[ "$output" == *"Python quickstart example discovery failed"* ]]
+ [[ "$output" == *"Python quickstart example discovery failed"* ]] || false
}
@test "main propagates RAG example discovery failure" {
@@ -334,7 +334,7 @@ create_fake_executable() {
run main
[ "$status" -ne 0 ]
- [[ "$output" == *"Python RAG example discovery failed"* ]]
+ [[ "$output" == *"Python RAG example discovery failed"* ]] || false
}
@test "main propagates RAG knowledge base setup failure" {
@@ -346,7 +346,7 @@ create_fake_executable() {
run main
[ "$status" -ne 0 ]
- [[ "$output" == *"Cannot run Python RAG examples because setup failed"* ]]
+ [[ "$output" == *"Cannot run Python RAG examples because setup failed"* ]]
|| false
}
@test "main skips RAG setup when no RAG examples exist" {
@@ -461,6 +461,6 @@ create_fake_executable() {
[ "$TEST_VENV_ACTIVATED" -eq 1 ]
[ "$PYFLINK_CLIENT_EXECUTABLE" = "$VENV_DIR/bin/python" ]
- [[ "$(cat "$PYTHON_CALLS")" == *"-m pip install --quiet"* ]]
- [[ "$(cat "$PYTHON_CALLS")" == *"apache-flink==$FLINK_VERSION"* ]]
+ [[ "$(cat "$PYTHON_CALLS")" == *"-m pip install --quiet"* ]] || false
+ [[ "$(cat "$PYTHON_CALLS")" == *"apache-flink==$FLINK_VERSION"* ]] || false
}