diff --git a/.github/workflows/1mb-start-tests.yml b/.github/workflows/1mb-start-tests.yml new file mode 100644 index 0000000..ae22c73 --- /dev/null +++ b/.github/workflows/1mb-start-tests.yml @@ -0,0 +1,49 @@ +name: 1MB start wrapper tests + +on: + push: + paths: + - "Resources/Server/1MB-start.sh" + - "Resources/Server/tests/1MB-start/**" + - ".github/workflows/1mb-start-tests.yml" + pull_request: + paths: + - "Resources/Server/1MB-start.sh" + - "Resources/Server/tests/1MB-start/**" + - ".github/workflows/1mb-start-tests.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install ShellCheck on macOS + if: runner.os == 'macOS' + run: command -v shellcheck >/dev/null 2>&1 || brew install shellcheck + + - name: Install ShellCheck on Ubuntu + if: runner.os == 'Linux' + run: | + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install --yes shellcheck + fi + + - name: Run syntax, behavior, and lint tests + shell: bash + run: /bin/bash Resources/Server/tests/1MB-start/run-tests.sh all diff --git a/Resources/Server/1MB-start.sh b/Resources/Server/1MB-start.sh index 1e959da..55f25f4 100755 --- a/Resources/Server/1MB-start.sh +++ b/Resources/Server/1MB-start.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # @Filename: 1MB-start.sh -# @Version: 2.17.9, build 082 for Minecraft 26.2 (Java 25, 64bit) +# @Version: 2.18.3, build 086 for Minecraft 26.2 (Java 25, 64bit) # @Release: August 4th, 2026 # @Description: Helps us start and fork a Minecraft 26.2 server session. # @Contact: I am @floris on Twitter, and mrfloris in MineCraft. @@ -18,7 +18,8 @@ ### _serverName="mcserver" -# Keep the name short, simple, and lowercase (no numbers or weird characters). +# Use 1-32 lowercase ASCII letters, numbers, hyphens or underscores. The first +# character must be a letter or number. # The name makes it easier to recognize the session in 'tmux ls', you can # re-attach to the forked sessions with 'tmux attach -t (name)'. @@ -30,6 +31,7 @@ _serverName="mcserver" ### _sibling="1MB-minecraft.sh" +_startupCheckSeconds=2 # Check process liveness only; this is not Paper readiness. _debug=true # Set to false to minimize output. ### FUNCTIONS AND CODE @@ -52,7 +54,7 @@ function _output { fi case "$_mode" in - oops|okay) _fd=2 ;; + oops) _fd=2 ;; esac if [ -z "${NO_COLOR+x}" ] && [ -t "$_fd" ]; then @@ -68,7 +70,7 @@ function _output { ;; okay) _args="${*:2}"; _prefix="(Info)"; - printf '\n%s%s%s %s%s\n' "$_yellow" "$_prefix" "$_cyan" "$_args" "$_reset" >&2; exit 1 + printf '\n%s%s%s %s%s\n' "$_yellow" "$_prefix" "$_cyan" "$_args" "$_reset"; return 0 ;; debug) _args="${*:2}"; _prefix="(Debug)"; @@ -131,46 +133,139 @@ function _showHelp { " --attach [name] Attach to the exact tmux session." \ " --help, -h Show this help." \ "" \ - "The session name defaults to '$_serverName'." + "The session name defaults to '$_serverName'." \ + "Names use 1-32 lowercase ASCII letters, numbers, hyphens or underscores." \ + "The first character must be a letter or number." } function _setServerName { - local _requestedName="${1:-}" - local _input="" + local _requestedName="${1-}" + local LC_ALL=C - if [ -n "$_requestedName" ]; then - _input=$(printf '%s\n' "$_requestedName" | awk '{ print tolower($1) }') - _input=$(printf '%s\n' "$_input" | awk '{ print substr($0, 1, 16) }') + if [ "${#_requestedName}" -gt 32 ] || ! [[ "$_requestedName" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then + _output oops "Invalid session name. Use 1-32 lowercase ASCII letters, numbers, hyphens or underscores. The first character must be a letter or number." + fi - if [[ "$_input" =~ ^[a-z]+$ ]]; then - _serverName="$_input" - else - _output oops "Provided input is invalid! Input is for a unique '_serverName'. Do not use numbers, spaces or weird chars. Keep it short, and a-z characters only." + _serverName="$_requestedName" +} + +function _isPaneId { + local _paneId="${1:-}" + local _paneNumber="" + + case "$_paneId" in + %*) _paneNumber="${_paneId#%}" ;; + *) return 1 ;; + esac + + case "$_paneNumber" in + ""|*[!0-9]*) return 1 ;; + *) return 0 ;; + esac +} + +function _verifyStartedPane { + local _paneId="${1:-}" + local _paneReport="" + local _paneDead="" + local _paneResult="" + local _paneStatus="" + local _paneSignal="" + local _exitDescription="" + + _output debug "Checking whether '$_sibling' remains active for $_startupCheckSeconds seconds ..." + + if ! sleep "$_startupCheckSeconds"; then + if ! tmux set-option -p -t "$_paneId" remain-on-exit off >/dev/null 2>&1; then + _output oops "The $_startupCheckSeconds-second startup check was interrupted, and tmux could not restore normal close-on-exit behavior. Inspect the '$_serverName' session before retrying." fi + _output oops "The $_startupCheckSeconds-second startup check was interrupted. The tmux session was not reported as started." + fi + + if ! _paneReport=$(tmux display-message -p -t "$_paneId" '#{pane_dead}|#{pane_dead_status}|#{pane_dead_signal}' 2>/dev/null); then + tmux set-option -p -t "$_paneId" remain-on-exit off >/dev/null 2>&1 || true + _output oops "'$_sibling' ended or its tmux pane disappeared during the $_startupCheckSeconds-second startup check. Run './$_sibling' directly from '$_scriptDir' to inspect its full output." fi + + _paneDead="${_paneReport%%|*}" + _paneResult="${_paneReport#*|}" + _paneStatus="${_paneResult%%|*}" + _paneSignal="${_paneResult#*|}" + + case "$_paneDead" in + 0) + if ! tmux set-option -p -t "$_paneId" remain-on-exit off >/dev/null 2>&1; then + _output oops "'$_sibling' remained active, but tmux could not restore normal close-on-exit behavior for pane '$_paneId'. Inspect the '$_serverName' session before retrying." + fi + + if ! _paneDead=$(tmux display-message -p -t "$_paneId" '#{pane_dead}' 2>/dev/null); then + _output oops "'$_sibling' ended as the $_startupCheckSeconds-second startup check completed. The tmux session was not reported as started." + fi + + if [ "$_paneDead" != 0 ]; then + tmux kill-pane -t "$_paneId" >/dev/null 2>&1 || true + _output oops "'$_sibling' ended as the $_startupCheckSeconds-second startup check completed. The tmux session was not reported as started." + fi + ;; + 1) + if [ -n "$_paneStatus" ]; then + _exitDescription="exit status $_paneStatus" + fi + if [ -n "$_paneSignal" ]; then + if [ -n "$_exitDescription" ]; then + _exitDescription="$_exitDescription, signal $_paneSignal" + else + _exitDescription="signal $_paneSignal" + fi + fi + [ -z "$_exitDescription" ] && _exitDescription="unknown exit status" + + tmux kill-pane -t "$_paneId" >/dev/null 2>&1 || true + _output oops "'$_sibling' exited during the $_startupCheckSeconds-second startup check ($_exitDescription). Run './$_sibling' directly from '$_scriptDir' to inspect its full output." + ;; + *) + tmux set-option -p -t "$_paneId" remain-on-exit off >/dev/null 2>&1 || true + _output oops "tmux returned an unexpected state for pane '$_paneId' during the startup check. The session was not reported as started." + ;; + esac } _action="start" -_nameArgument="${1:-}" +_nameArgument="$_serverName" case "${1:-}" in -h|--help) + [ "$#" -eq 1 ] || _output oops "Unexpected extra arguments. Run '$0 --help' for usage." _showHelp exit 0 ;; --status) + [ "$#" -le 2 ] || _output oops "Unexpected extra arguments. Run '$0 --help' for usage." _action="status" - _nameArgument="${2:-}" + if [ "$#" -eq 2 ]; then + _nameArgument="$2" + fi ;; --attach) + [ "$#" -le 2 ] || _output oops "Unexpected extra arguments. Run '$0 --help' for usage." _action="attach" - _nameArgument="${2:-}" + if [ "$#" -eq 2 ]; then + _nameArgument="$2" + fi ;; --*) _output oops "Unknown option '$1'. Run '$0 --help' for usage." ;; +*) + [ "$#" -le 1 ] || _output oops "Unexpected extra arguments. Run '$0 --help' for usage." + if [ "$#" -eq 1 ]; then + _nameArgument="$1" + fi + ;; esac +_setServerName "$_nameArgument" + [ "$EUID" -eq 0 ] && _output oops "*!* This script should not be run using sudo, or as the root user!" if [ "$_action" = start ]; then @@ -182,8 +277,6 @@ if [ "$_action" = start ]; then fi fi -_setServerName "$_nameArgument" - if ! type "tmux" >/dev/null 2>&1; then _output oops "'tmux' is required but was not found. On macOS, install it with: brew install tmux" fi @@ -214,9 +307,21 @@ _output debug "Using server directory: $_scriptDir" _output debug "Found 'tmux', attempting to start '$_sibling' in a detached tmux session ..." # Deliberately launch only ./1MB-minecraft.sh from the wrapper's own directory. -if ! tmux new-session -d -s "$_serverName" -c "$_scriptDir" "exec \"./$_sibling\""; then - _output oops "Could not create the '$_serverName' tmux session and start '$_sibling'. Review the tmux error above and check with 'tmux ls'." +_paneId="" +if ! _paneId=$(tmux new-session -d -P -F '#{pane_id}' -s "$_serverName" -c "$_scriptDir" "exec \"./$_sibling\"" \; set-option -p -t "=$_serverName:" remain-on-exit on); then + if _isPaneId "$_paneId"; then + tmux kill-pane -t "$_paneId" >/dev/null 2>&1 || true + fi + _output oops "Could not create the '$_serverName' tmux session and prepare its startup check. Review the tmux error above and check with 'tmux ls'." +fi + +if ! _isPaneId "$_paneId"; then + tmux kill-session -t "=$_serverName" >/dev/null 2>&1 || true + _output oops "tmux created the '$_serverName' session but did not return a valid pane identifier, so the new session was closed." fi + +_verifyStartedPane "$_paneId" + [[ "$_debug" == true ]] && tmux ls; _output debug "To re-attach: tmux attach -t $_serverName" _output debug "tmux session started." diff --git a/Resources/Server/1mb-start-todo.md b/Resources/Server/1mb-start-todo.md index 298295b..54fe8ec 100644 --- a/Resources/Server/1mb-start-todo.md +++ b/Resources/Server/1mb-start-todo.md @@ -6,16 +6,16 @@ as deliberate follow-up changes. ## Known live baseline -- Script version: `2.16.3`, build `072` +- Script version: `2.17.9`, build `082` - Release date: August 4, 2026 - Status: working, tested, and live in August 2026 -- Git baseline: `a2dc6cc8f5d14062b848c87389859e1eab1e4e48` +- Git baseline: `c5f6c8c5c5ee4545ec4d0dcccaa46e8cc2fc52dc` - Baseline source: - `git show a2dc6cc8f5d14062b848c87389859e1eab1e4e48:Resources/Server/1MB-start.sh` + `git show c5f6c8c5c5ee4545ec4d0dcccaa46e8cc2fc52dc:Resources/Server/1MB-start.sh` The baseline records the proven production version; it is not a claim that the -script was free of edge-case defects. New work begins with version `2.17.0`, -build `073`. +script is free of edge-case defects. Follow-up work continues with version +`2.18.3`, build `086`. ## Completed in 2.17.0 build 073 @@ -111,6 +111,47 @@ scoped fix. The file's missing final newline was normalized mechanically. - [x] Kept sibling resolution and validation exclusive to normal startup, so existing sessions remain inspectable when the sibling file is unavailable. +## Completed in 2.18.0 build 083 + +- [x] Added a two-second liveness check for the exact tmux pane created for + `1MB-minecraft.sh`; attach guidance and success output now wait for it. +- [x] Temporarily retain only that new pane during the check, allowing immediate + clean exits, nonzero statuses, and terminating signals to be reported. +- [x] Restore `remain-on-exit=off` after a successful check so the tmux session + still closes normally whenever `1MB-minecraft.sh` later exits. +- [x] Keep the check explicitly bounded to process liveness; it does not claim + that Paper completed startup. + +## Completed in 2.18.1 build 084 + +- [x] Successful `_output okay` messages now write to stdout, return status + `0`, and allow the caller to continue instead of terminating the wrapper. +- [x] The fatal `_output oops` path remains unchanged: it writes to stderr and + exits with status `1`. + +## Completed in 2.18.2 build 085 + +- [x] Validate the complete requested session name without lowercasing, + splitting or truncating it. Names use 1-32 lowercase ASCII letters, numbers, + hyphens or underscores and must begin with a letter or number. +- [x] Validate the configured default name through the same path and reject + empty, uppercase, overlong or otherwise invalid names with a clear error. +- [x] Reject unexpected extra arguments for startup, `--help`, `--status` and + `--attach` instead of silently ignoring them. + +## Completed in 2.18.3 build 086 + +- [x] Added a committed Bash 3.2-compatible test runner with syntax, behavioral + and ShellCheck modes under `Resources/Server/tests/1MB-start/`. +- [x] Added scenario-driven fake `tmux` and instant fake `sleep` executables. + Disposable fixtures exercise startup success and failure without contacting + a real tmux server or launching Paper. +- [x] Covered exact atomic launch arguments, duplicate sessions, pane and child + exit states, probe-boundary races, status and attach behavior, sibling and + dependency failures, session names, argument arity, and output semantics. +- [x] Added a path-filtered GitHub Actions workflow that runs the complete suite + with ShellCheck on current macOS and Ubuntu hosted runners. + ## Critical - [x] Stop immediately when tmux session creation fails; never send startup @@ -128,9 +169,9 @@ scoped fix. The file's missing final newline was normalized mechanically. - [x] Replace the two-stage `tmux new` plus `tmux send-keys` launch with one checked, atomic `tmux new-session -d` command that starts the sibling directly. Completed in `2.17.2`, build `075`. -- [ ] Detect and report when the child exits immediately after tmux successfully - creates the session. The tmux command confirms session creation, not that - Paper completed startup. +- [x] Detect and report when the child exits immediately after tmux successfully + creates the session. Completed in `2.18.0`, build `083`, with a two-second + exact-pane liveness check that does not claim Paper completed startup. - [x] Fix `_debug=false`: disabled debug messages now return success, so an otherwise successful wrapper exits successfully. Completed in `2.17.5`, build `078`. @@ -142,9 +183,10 @@ scoped fix. The file's missing final newline was normalized mechanically. ## Medium priority -- [ ] Stop splitting and truncating the requested session name before +- [x] Stop splitting and truncating the requested session name before validation. Validate the complete input and length, reject invalid or - overlong names, and reject unexpected extra arguments. + overlong names, and reject unexpected extra arguments. Completed in + `2.18.2`, build `085`. - [x] Close the tmux session when `1MB-minecraft.sh` exits. Approved and completed in `2.17.2`, build `075`; the launcher is now the pane's direct process. @@ -163,8 +205,8 @@ scoped fix. The file's missing final newline was normalized mechanically. build `077`. - [x] Make output-function variables local and remove the undefined `B` and `X` colour variables. Completed in `2.17.4`, build `077`. -- [ ] Correct the `okay` branch so a successful status message does not exit - with status `1`. +- [x] Correct the `okay` branch so a successful status message does not exit + with status `1`. Completed in `2.18.1`, build `084`. - [x] Emit ANSI colours only to an interactive terminal and honor `NO_COLOR` so launchd/systemd logs remain clean. Completed in `2.17.4`, build `077`. - [x] Verify that the sibling is a regular readable and executable file, and @@ -188,9 +230,10 @@ scoped fix. The file's missing final newline was normalized mechanically. `set -o pipefail` can then be evaluated safely. - [x] Add small `--help`, `--status`, and `--attach` commands. Completed in `2.17.9`, build `082`; `--version` was intentionally not added. -- [ ] Add repeatable checks with `bash -n`, ShellCheck, and disposable fake or +- [x] Add repeatable checks with `bash -n`, ShellCheck, and disposable fake or isolated tmux sessions for success, duplicate-name, missing-sibling, and - missing-dependency paths. + missing-dependency paths. Completed in `2.18.3`, build `086`, with committed + local tests and path-filtered macOS/Ubuntu CI. - [ ] Consider an optional `umask 027` only after auditing backup, web, and group-access requirements. Do not change inherited Paper file permissions as part of an unrelated launcher update. @@ -213,7 +256,8 @@ scoped fix. The file's missing final newline was normalized mechanically. ## Compatibility decisions before the next larger revision -1. Confirm whether session names remain letters-only or expand to a documented - safe character set. +1. [x] Session names use a documented safe set: 1-32 lowercase ASCII letters, + numbers, hyphens or underscores, beginning with a letter or number. + Completed in `2.18.2`, build `085`. 2. Confirm which quality-of-life commands, if any, belong in this intentionally small wrapper. diff --git a/Resources/Server/tests/1MB-start/README.md b/Resources/Server/tests/1MB-start/README.md new file mode 100644 index 0000000..7ff28a6 --- /dev/null +++ b/Resources/Server/tests/1MB-start/README.md @@ -0,0 +1,52 @@ +# `1MB-start.sh` tests + +This suite tests `../../1MB-start.sh` without starting Paper or contacting a +real tmux server. Every behavioral test runs against disposable copies under a +temporary directory and prepends the committed fake `tmux` and `sleep` +executables to `PATH`. The runner refuses to continue unless those exact fakes +resolve first and also assigns a disposable `TMUX_TMPDIR` as a second isolation +layer. + +Run the complete suite from the repository root: + +```bash +Resources/Server/tests/1MB-start/run-tests.sh +``` + +The default `all` mode runs: + +- `bash -n` with macOS's Bash 3.2 and any supported modern Bash found locally; +- fake-tmux behavioral tests with every discovered Bash interpreter; +- ShellCheck over the production wrapper, runner, and fake executables. + +ShellCheck is the only additional dependency for `all` and `lint` mode: + +```bash +# macOS +brew install shellcheck + +# Ubuntu +sudo apt install shellcheck +``` + +Individual modes are available when needed: + +```bash +# Bash syntax and fake-tmux behavior only +Resources/Server/tests/1MB-start/run-tests.sh test + +# ShellCheck only +Resources/Server/tests/1MB-start/run-tests.sh lint +``` + +The behavior suite covers healthy startup, exact tmux targets, duplicate +sessions, child exit statuses and signals, probe-boundary failures, +`remain-on-exit` restoration, missing or invalid siblings, missing tmux, +status and attach behavior, session-name validation, argument validation, and +output return/stream behavior. The fake `sleep` makes the two-second liveness +probe complete immediately. + +Tests must not be run with `sudo`: the production wrapper deliberately refuses +to run as root. When a test fails, its disposable state and tmux call log are +retained and their location is printed. Successful runs remove their temporary +state. diff --git a/Resources/Server/tests/1MB-start/fake-bin/sleep b/Resources/Server/tests/1MB-start/fake-bin/sleep new file mode 100755 index 0000000..1dc7ee8 --- /dev/null +++ b/Resources/Server/tests/1MB-start/fake-bin/sleep @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Instant sleep replacement for deterministic startup-probe tests. + +_stateDir="${FAKE_TMUX_STATE_DIR:-}" +_scenario="${FAKE_TMUX_SCENARIO:-healthy}" + +if [ -z "$_stateDir" ] || [ ! -d "$_stateDir" ]; then + printf '%s\n' "fake sleep: FAKE_TMUX_STATE_DIR is missing or invalid" >&2 + exit 90 +fi + +printf '%s\n' "$*" >>"$_stateDir/sleep.log" + +case "$_scenario" in +sleep-failure|sleep-failure-restore-failure) exit 1 ;; +*) exit 0 ;; +esac diff --git a/Resources/Server/tests/1MB-start/fake-bin/tmux b/Resources/Server/tests/1MB-start/fake-bin/tmux new file mode 100755 index 0000000..59fa74f --- /dev/null +++ b/Resources/Server/tests/1MB-start/fake-bin/tmux @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +# Scenario-driven tmux replacement used only by the 1MB-start.sh test suite. +# It must never write diagnostics to stdout for new-session because stdout is +# the pane-ID channel consumed by the wrapper. + +_stateDir="${FAKE_TMUX_STATE_DIR:-}" +_scenario="${FAKE_TMUX_SCENARIO:-healthy}" + +if [ -z "$_stateDir" ] || [ ! -d "$_stateDir" ]; then + printf '%s\n' "fake tmux: FAKE_TMUX_STATE_DIR is missing or invalid" >&2 + exit 90 +fi + +_logFile="$_stateDir/tmux.log" +_command="${1:-}" + +printf '%s' "$_command" >>"$_logFile" +shift || true +for _argument in "$@"; do + printf '\t%s' "$_argument" >>"$_logFile" +done +printf '\n' >>"$_logFile" + +case "$_command" in +new-session) + case "$_scenario" in + duplicate) + printf '%s\n' "duplicate session: mcserver" >&2 + exit 1 + ;; + new-failure-with-pane) + printf '%s\n' '%42' + exit 1 + ;; + new-failure) + exit 1 + ;; + empty-pane) + exit 0 + ;; + malformed-pane) + printf '%s\n' 'not-a-pane' + exit 0 + ;; + *) + printf '%s\n' '%42' + exit 0 + ;; + esac + ;; +has-session) + case "$_scenario" in + status-running|attach-running) exit 0 ;; + *) exit 1 ;; + esac + ;; +attach-session) + case "$_scenario" in + attach-running) exit 23 ;; + *) exit 1 ;; + esac + ;; +display-message) + _format="" + for _argument in "$@"; do + _format="$_argument" + done + + case "$_format" in + '#{pane_dead}|#{pane_dead_status}|#{pane_dead_signal}') + case "$_scenario" in + first-display-missing) exit 1 ;; + child-exit-zero) printf '%s\n' '1|0|' ;; + child-exit-seven) printf '%s\n' '1|7|' ;; + child-signal-fifteen) printf '%s\n' '1||15' ;; + child-unknown-exit) printf '%s\n' '1||' ;; + unexpected-pane-state) printf '%s\n' 'unexpected||' ;; + *) printf '%s\n' '0||' ;; + esac + ;; + '#{pane_dead}') + case "$_scenario" in + boundary-disappeared) exit 1 ;; + boundary-dead) printf '%s\n' '1' ;; + *) printf '%s\n' '0' ;; + esac + ;; + *) + printf '%s\n' "fake tmux: unexpected display format: $_format" >&2 + exit 91 + ;; + esac + ;; +set-option) + case "$_scenario" in + restore-failure|sleep-failure-restore-failure) exit 1 ;; + *) exit 0 ;; + esac + ;; +kill-pane|kill-session) + exit 0 + ;; +ls) + case "$_scenario" in + list-failure) exit 1 ;; + *) + printf '%s\n' 'fake-session: 1 windows (created for tests)' + exit 0 + ;; + esac + ;; +*) + printf '%s\n' "fake tmux: unexpected command: $_command" >&2 + exit 92 + ;; +esac diff --git a/Resources/Server/tests/1MB-start/run-tests.sh b/Resources/Server/tests/1MB-start/run-tests.sh new file mode 100755 index 0000000..49e1d7b --- /dev/null +++ b/Resources/Server/tests/1MB-start/run-tests.sh @@ -0,0 +1,743 @@ +#!/usr/bin/env bash + +# Repeatable, non-production tests for Resources/Server/1MB-start.sh. +# Compatible with the Bash 3.2 shipped by macOS. + +_suiteDir=$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P) || exit 1 +_serverDir=$(cd -P -- "$_suiteDir/../.." >/dev/null 2>&1 && pwd -P) || exit 1 +_target="$_serverDir/1MB-start.sh" +_runner="$_suiteDir/run-tests.sh" +_fakeTmux="$_suiteDir/fake-bin/tmux" +_fakeSleep="$_suiteDir/fake-bin/sleep" +_originalPath="$PATH" +_mode="${1:-all}" + +_passed=0 +_failed=0 +_skipped=0 +_caseNumber=0 +_caseFailed=0 +_caseLabel="" +_tempRoot="" +_bashUnderTest="" +_caseDir="" +_caseServerDir="" +_caseStateDir="" +_caseWrapper="" +_stdoutFile="" +_stderrFile="" +_runStatus=0 + +function _usage { + printf '%s\n' \ + "Usage: ${0##*/} [all|test|lint]" \ + "" \ + " all Run syntax, fake-tmux behavior, and ShellCheck (default)." \ + " test Run syntax and fake-tmux behavior without ShellCheck." \ + " lint Run ShellCheck only." +} + +if [ "$#" -gt 1 ]; then + _usage >&2 + exit 2 +fi + +case "$_mode" in +all|test|lint) + ;; +-h|--help) + _usage + exit 0 + ;; +*) + _usage >&2 + exit 2 + ;; +esac + +function _pass { + _passed=$((_passed + 1)) + printf 'PASS: %s\n' "$1" +} + +function _fail { + _failed=$((_failed + 1)) + printf 'FAIL: %s\n' "$1" >&2 +} + +function _skip { + _skipped=$((_skipped + 1)) + printf 'SKIP: %s\n' "$1" +} + +function _beginCase { + _caseLabel="$1" + _caseFailed=0 +} + +function _problem { + _caseFailed=1 + printf ' %s\n' "$1" >&2 + if [ -n "$_caseDir" ]; then + printf ' fixture: %s\n' "$_caseDir" >&2 + fi +} + +function _showFile { + local _label="$1" + local _file="$2" + + if [ -s "$_file" ]; then + printf ' %s:\n' "$_label" >&2 + sed 's/^/ /' "$_file" >&2 + fi +} + +function _finishCase { + if [ "$_caseFailed" -eq 0 ]; then + _pass "$_caseLabel" + return 0 + fi + + _showFile "stdout" "$_stdoutFile" + _showFile "stderr" "$_stderrFile" + if [ -n "$_caseStateDir" ]; then + _showFile "fake tmux calls" "$_caseStateDir/tmux.log" + _showFile "fake sleep calls" "$_caseStateDir/sleep.log" + fi + _fail "$_caseLabel" + return 1 +} + +function _cleanup { + if [ -z "$_tempRoot" ] || [ ! -d "$_tempRoot" ]; then + return + fi + + if [ "$_failed" -eq 0 ]; then + rm -rf -- "$_tempRoot" + else + printf 'Failed-test artifacts retained at: %s\n' "$_tempRoot" >&2 + fi +} + +trap _cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +function _addBash { + local _candidate="$1" + local _candidateDir="" + local _candidatePath="" + local _known="" + + [ -x "$_candidate" ] || return 0 + _candidateDir=$(cd -P -- "$(dirname -- "$_candidate")" >/dev/null 2>&1 && pwd -P) || return 0 + _candidatePath="$_candidateDir/$(basename -- "$_candidate")" + + for _known in "${_bashPaths[@]}"; do + [ "$_known" = "$_candidatePath" ] && return 0 + done + _bashPaths[${#_bashPaths[@]}]="$_candidatePath" +} + +function _discoverBashes { + local _pathBash="" + + _bashPaths=() + _addBash /bin/bash + _pathBash=$(command -v bash 2>/dev/null || true) + [ -n "$_pathBash" ] && _addBash "$_pathBash" + _addBash /opt/homebrew/bin/bash + _addBash /usr/local/bin/bash +} + +function _newFixture { + local _siblingMode="${1:-executable}" + + _caseNumber=$((_caseNumber + 1)) + _caseDir="$_tempRoot/case-$_caseNumber" + _caseServerDir="$_caseDir/server directory with spaces" + _caseStateDir="$_caseDir/state" + _caseWrapper="$_caseServerDir/1MB-start.sh" + _stdoutFile="$_caseDir/stdout" + _stderrFile="$_caseDir/stderr" + + mkdir -p "$_caseServerDir" "$_caseStateDir/tmux-socket" + cp "$_target" "$_caseWrapper" + chmod 0755 "$_caseWrapper" + + case "$_siblingMode" in + executable) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$_caseServerDir/1MB-minecraft.sh" + chmod 0755 "$_caseServerDir/1MB-minecraft.sh" + ;; + nonexecutable) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$_caseServerDir/1MB-minecraft.sh" + chmod 0644 "$_caseServerDir/1MB-minecraft.sh" + ;; + unreadable) + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$_caseServerDir/1MB-minecraft.sh" + chmod 0111 "$_caseServerDir/1MB-minecraft.sh" + ;; + directory) + mkdir "$_caseServerDir/1MB-minecraft.sh" + ;; + symlink) + ln -s "1MB-start.sh" "$_caseServerDir/1MB-minecraft.sh" + ;; + missing) + ;; + *) + printf '%s\n' "Unknown sibling fixture mode: $_siblingMode" >&2 + exit 2 + ;; + esac +} + +function _runWrapper { + local _scenario="$1" + shift + + : >"$_stdoutFile" + : >"$_stderrFile" + FAKE_TMUX_SCENARIO="$_scenario" \ + FAKE_TMUX_STATE_DIR="$_caseStateDir" \ + NO_COLOR=1 \ + PATH="$_suiteDir/fake-bin:$_originalPath" \ + TMUX='' \ + TMUX_TMPDIR="$_caseStateDir/tmux-socket" \ + "$_bashUnderTest" "$_caseWrapper" "$@" >"$_stdoutFile" 2>"$_stderrFile" + _runStatus=$? +} + +function _expectStatus { + local _expected="$1" + if [ "$_runStatus" -ne "$_expected" ]; then + _problem "expected exit $_expected, got $_runStatus" + fi +} + +function _expectEmpty { + local _file="$1" + local _description="$2" + [ ! -s "$_file" ] || _problem "expected empty $_description" +} + +function _expectContains { + local _file="$1" + local _text="$2" + local _description="$3" + grep -Fq "$_text" "$_file" || _problem "missing $_description: $_text" +} + +function _expectNotContains { + local _file="$1" + local _text="$2" + local _description="$3" + if grep -Fq "$_text" "$_file"; then + _problem "unexpected $_description: $_text" + fi +} + +function _expectNoTmuxCall { + if [ -s "$_caseStateDir/tmux.log" ]; then + _problem "tmux was called unexpectedly" + fi +} + +function _expectLogLine { + local _line="$1" + if ! grep -Fqx "$_line" "$_caseStateDir/tmux.log" 2>/dev/null; then + _problem "missing exact tmux call: $_line" + fi +} + +function _expectNoLogCommand { + local _command="$1" + if grep -q "^${_command}" "$_caseStateDir/tmux.log" 2>/dev/null; then + _problem "unexpected tmux command: $_command" + fi +} + +function _setExpectedLaunch { + local _name="$1" + printf -v _expectedLaunch 'new-session\t-d\t-P\t-F\t#{pane_id}\t-s\t%s\t-c\t%s\texec "./1MB-minecraft.sh"\t;\tset-option\t-p\t-t\t=%s:\tremain-on-exit\ton' \ + "$_name" "$_caseServerDir" "$_name" +} + +function _expectFilesEqual { + local _expected="$1" + local _actual="$2" + local _description="$3" + if ! cmp -s "$_expected" "$_actual"; then + _problem "$_description differs" + diff -u "$_expected" "$_actual" >&2 || true + fi +} + +function _testHealthyStart { + local _expectedLog="" + local _escape="" + + _beginCase "healthy startup uses exact atomic tmux calls" + _newFixture executable + _runWrapper healthy mcserver-2 + _expectStatus 0 + _expectEmpty "$_stderrFile" "stderr" + _expectContains "$_stdoutFile" "To re-attach: tmux attach -t mcserver-2" "attach guidance" + _expectContains "$_stdoutFile" "tmux session started." "success message" + _escape=$(printf '\033') + _expectNotContains "$_stdoutFile" "$_escape" "ANSI escape" + _setExpectedLaunch mcserver-2 + _expectedLog="$_caseDir/expected-tmux.log" + printf '%s\n' \ + "$_expectedLaunch" \ + $'display-message\t-p\t-t\t%42\t#{pane_dead}|#{pane_dead_status}|#{pane_dead_signal}' \ + $'set-option\t-p\t-t\t%42\tremain-on-exit\toff' \ + $'display-message\t-p\t-t\t%42\t#{pane_dead}' \ + 'ls' >"$_expectedLog" + _expectFilesEqual "$_expectedLog" "$_caseStateDir/tmux.log" "tmux call order" + printf '%s\n' '2' >"$_caseDir/expected-sleep.log" + _expectFilesEqual "$_caseDir/expected-sleep.log" "$_caseStateDir/sleep.log" "sleep call" + _finishCase +} + +function _testConfiguredDefaultName { + _beginCase "omitted name validates and preserves the configured default" + + _newFixture executable + _runWrapper healthy + _expectStatus 0 + _setExpectedLaunch mcserver + _expectLogLine "$_expectedLaunch" + _expectContains "$_stdoutFile" "tmux attach -t mcserver" "default attach guidance" + + _newFixture executable + _runWrapper status-running --status + _expectStatus 0 + _expectLogLine $'has-session\t-t\t=mcserver' + _expectContains "$_stdoutFile" "tmux session 'mcserver' is running" "default status" + _finishCase +} + +function _testListFailureIsNonfatal { + _beginCase "tmux list failure does not redefine startup success" + _newFixture executable + _runWrapper list-failure mcserver + _expectStatus 0 + _expectContains "$_stdoutFile" "tmux session started." "success message" + _expectLogLine 'ls' + _finishCase +} + +function _testDuplicateSession { + _beginCase "duplicate session fails without mutating an existing session" + _newFixture executable + _runWrapper duplicate mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "duplicate session" "tmux duplicate error" + _expectContains "$_stderrFile" "Could not create" "wrapper failure" + _expectNotContains "$_stdoutFile" "tmux session started." "success message" + _expectNoLogCommand kill-pane + _expectNoLogCommand kill-session + _expectNoLogCommand send-keys + _finishCase +} + +function _testNewFailureWithPane { + _beginCase "partial new-session failure cleans only the returned pane" + _newFixture executable + _runWrapper new-failure-with-pane mcserver + _expectStatus 1 + _expectLogLine $'kill-pane\t-t\t%42' + _expectNoLogCommand kill-session + _finishCase +} + +function _testInvalidPane { + local _scenario="$1" + local _label="$2" + + _beginCase "$_label" + _newFixture executable + _runWrapper "$_scenario" mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "did not return a valid pane identifier" "invalid-pane error" + _expectLogLine $'kill-session\t-t\t=mcserver' + _expectNotContains "$_stdoutFile" "tmux session started." "success message" + _finishCase +} + +function _testChildExit { + local _scenario="$1" + local _expectedText="$2" + local _label="$3" + + _beginCase "$_label" + _newFixture executable + _runWrapper "$_scenario" mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "$_expectedText" "exit description" + _expectLogLine $'kill-pane\t-t\t%42' + _expectNoLogCommand ls + _expectNotContains "$_stdoutFile" "tmux session started." "success message" + _finishCase +} + +function _testFirstDisplayMissing { + _beginCase "disappearing pane fails and restores close-on-exit best-effort" + _newFixture executable + _runWrapper first-display-missing mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "pane disappeared" "missing-pane error" + _expectLogLine $'set-option\t-p\t-t\t%42\tremain-on-exit\toff' + _expectNoLogCommand ls + _finishCase +} + +function _testUnexpectedPaneState { + _beginCase "unexpected pane state fails closed" + _newFixture executable + _runWrapper unexpected-pane-state mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "unexpected state" "unexpected-state error" + _expectLogLine $'set-option\t-p\t-t\t%42\tremain-on-exit\toff' + _expectNoLogCommand ls + _finishCase +} + +function _testRestoreFailure { + _beginCase "failure to restore close-on-exit prevents success" + _newFixture executable + _runWrapper restore-failure mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "could not restore normal close-on-exit behavior" "restore error" + _expectNoLogCommand ls + _finishCase +} + +function _testBoundaryResult { + local _scenario="$1" + local _label="$2" + + _beginCase "$_label" + _newFixture executable + _runWrapper "$_scenario" mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "ended as the 2-second startup check completed" "boundary error" + _expectNoLogCommand ls + if [ "$_scenario" = boundary-dead ]; then + _expectLogLine $'kill-pane\t-t\t%42' + else + _expectNoLogCommand kill-pane + fi + _finishCase +} + +function _testSleepFailure { + local _scenario="$1" + local _expectedText="$2" + local _label="$3" + + _beginCase "$_label" + _newFixture executable + _runWrapper "$_scenario" mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "$_expectedText" "sleep interruption error" + _expectLogLine $'set-option\t-p\t-t\t%42\tremain-on-exit\toff' + _expectNoLogCommand display-message + _expectNoLogCommand ls + printf '%s\n' '2' >"$_caseDir/expected-sleep.log" + _expectFilesEqual "$_caseDir/expected-sleep.log" "$_caseStateDir/sleep.log" "sleep call" + _finishCase +} + +function _testStatusCommands { + _beginCase "status uses an exact session target and meaningful statuses" + + _newFixture executable + _runWrapper status-running --status mcserver-2 + _expectStatus 0 + _expectContains "$_stdoutFile" "'mcserver-2' is running" "running status" + _expectLogLine $'has-session\t-t\t=mcserver-2' + + _newFixture executable + _runWrapper status-missing --status mcserver-2 + _expectStatus 1 + _expectContains "$_stdoutFile" "'mcserver-2' is not running" "missing status" + _expectLogLine $'has-session\t-t\t=mcserver-2' + _finishCase +} + +function _testAttachCommands { + _beginCase "attach uses exec, an exact target, and preserves tmux status" + + _newFixture executable + _runWrapper attach-running --attach mcserver_3 + _expectStatus 23 + _expectLogLine $'has-session\t-t\t=mcserver_3' + _expectLogLine $'attach-session\t-t\t=mcserver_3' + + _newFixture executable + _runWrapper attach-missing --attach mcserver_3 + _expectStatus 1 + _expectContains "$_stderrFile" "No tmux session named 'mcserver_3'" "missing attach error" + _expectLogLine $'has-session\t-t\t=mcserver_3' + _expectNoLogCommand attach-session + _finishCase +} + +function _testSiblingValidation { + local _mode="" + + _beginCase "invalid sibling file types and permissions fail before tmux" + for _mode in missing nonexecutable unreadable directory symlink; do + _newFixture "$_mode" + _runWrapper healthy mcserver + _expectStatus 1 + _expectContains "$_stderrFile" "must be a regular, non-symlink, readable and executable file" "sibling validation" + _expectNoTmuxCall + done + _finishCase +} + +function _testMissingTmux { + local _emptyPath="" + + _beginCase "missing tmux fails clearly without falling back" + _newFixture executable + _emptyPath="$_caseDir/empty-path" + mkdir -p "$_emptyPath" + : >"$_stdoutFile" + : >"$_stderrFile" + NO_COLOR=1 \ + PATH="$_emptyPath" \ + TMUX='' \ + TMUX_TMPDIR="$_caseStateDir/tmux-socket" \ + "$_bashUnderTest" "$_caseWrapper" --status mcserver >"$_stdoutFile" 2>"$_stderrFile" + _runStatus=$? + _expectStatus 1 + _expectContains "$_stderrFile" "'tmux' is required but was not found" "missing-tmux error" + _expectNoTmuxCall + _finishCase +} + +function _testNames { + local _name="" + local _validNames=(a 0 mcserver1 mcserver-2 mcserver_3 1mb abcdefghijklmnopq a1234567890123456789012345678901) + local _invalidNames=("" MCServer -server _server " mcserver" "mcserver " "mc server" $'mc\tserver' $'mc\nserver' mcserver:1 mcserver.1 'mcserver*' 'mcserver?' mc/server '=mcserver' "\$1" @mcserver +mcserver a12345678901234567890123456789012 'mcsérver') + + _beginCase "complete session names are preserved or rejected without tmux" + for _name in "${_validNames[@]}"; do + _newFixture executable + _runWrapper status-running --status "$_name" + _expectStatus 0 + _expectContains "$_stdoutFile" "tmux session '$_name' is running" "preserved session name" + _expectLogLine "has-session"$'\t'"-t"$'\t'"=$_name" + done + + for _name in "${_invalidNames[@]}"; do + _newFixture executable + _runWrapper status-running --status "$_name" + _expectStatus 1 + _expectContains "$_stderrFile" "Invalid session name." "invalid-name error" + _expectNoTmuxCall + done + _finishCase +} + +function _testArgumentsAndHelp { + _beginCase "help works and surplus or unknown arguments fail before tmux" + + _newFixture executable + _runWrapper healthy --help + _expectStatus 0 + _expectContains "$_stdoutFile" "Names use 1-32 lowercase ASCII letters" "name help" + _expectNoTmuxCall + + _newFixture executable + _runWrapper healthy --help extra + _expectStatus 1 + _expectContains "$_stderrFile" "Unexpected extra arguments." "help arity error" + _expectNoTmuxCall + + _newFixture executable + _runWrapper healthy --status mcserver extra + _expectStatus 1 + _expectContains "$_stderrFile" "Unexpected extra arguments." "status arity error" + _expectNoTmuxCall + + _newFixture executable + _runWrapper healthy --attach mcserver extra + _expectStatus 1 + _expectContains "$_stderrFile" "Unexpected extra arguments." "attach arity error" + _expectNoTmuxCall + + _newFixture executable + _runWrapper healthy mcserver extra + _expectStatus 1 + _expectContains "$_stderrFile" "Unexpected extra arguments." "startup arity error" + _expectNoTmuxCall + + _newFixture executable + _runWrapper healthy --unknown + _expectStatus 1 + _expectContains "$_stderrFile" "Unknown option '--unknown'" "unknown-option error" + _expectNoTmuxCall + _finishCase +} + +function _testOutputHelper { + local _fragment="" + local _escape="" + + _beginCase "output helper preserves success, failure, debug, and no-color behavior" + _newFixture executable + _fragment="$_caseDir/output-function.sh" + sed -n '/^function _output {$/,/^}$/p' "$_target" >"$_fragment" + + : >"$_stdoutFile" + : >"$_stderrFile" + # The nested Bash, not this runner, deliberately expands these variables. + # shellcheck disable=SC2016 + "$_bashUnderTest" -c '_debug=true; unset NO_COLOR; source "$1"; _output okay "successful message"; result=$?; printf "continued:%s\n" "$result"; exit "$result"' _ "$_fragment" >"$_stdoutFile" 2>"$_stderrFile" + _runStatus=$? + _expectStatus 0 + _expectContains "$_stdoutFile" "successful message" "okay output" + _expectContains "$_stdoutFile" "continued:0" "caller continuation" + _expectEmpty "$_stderrFile" "okay stderr" + _escape=$(printf '\033') + _expectNotContains "$_stdoutFile" "$_escape" "non-TTY ANSI escape" + + : >"$_stdoutFile" + : >"$_stderrFile" + # shellcheck disable=SC2016 + "$_bashUnderTest" -c '_debug=true; source "$1"; _output oops "fatal message"; printf "unreachable\n"' _ "$_fragment" >"$_stdoutFile" 2>"$_stderrFile" + _runStatus=$? + _expectStatus 1 + _expectEmpty "$_stdoutFile" "oops stdout" + _expectContains "$_stderrFile" "fatal message" "oops output" + _expectNotContains "$_stderrFile" "unreachable" "post-oops output" + + : >"$_stdoutFile" + : >"$_stderrFile" + # shellcheck disable=SC2016 + "$_bashUnderTest" -c '_debug=false; source "$1"; _output debug "hidden"; result=$?; exit "$result"' _ "$_fragment" >"$_stdoutFile" 2>"$_stderrFile" + _runStatus=$? + _expectStatus 0 + _expectEmpty "$_stdoutFile" "disabled debug stdout" + _expectEmpty "$_stderrFile" "disabled debug stderr" + _finishCase +} + +function _runBehaviorForBash { + local _version="" + + _bashUnderTest="$1" + _version=$("$_bashUnderTest" --version | sed -n '1p') + printf '\nBehavior tests with %s\n' "$_version" + + _testHealthyStart + _testConfiguredDefaultName + _testListFailureIsNonfatal + _testDuplicateSession + _testNewFailureWithPane + _testInvalidPane empty-pane "empty pane identifier closes only the new session" + _testInvalidPane malformed-pane "malformed pane identifier closes only the new session" + _testChildExit child-exit-zero "exit status 0" "immediate clean child exit is reported" + _testChildExit child-exit-seven "exit status 7" "immediate nonzero child exit is reported" + _testChildExit child-signal-fifteen "signal 15" "immediate signal termination is reported" + _testChildExit child-unknown-exit "unknown exit status" "missing child exit metadata is reported" + _testFirstDisplayMissing + _testUnexpectedPaneState + _testRestoreFailure + _testBoundaryResult boundary-dead "death at the probe boundary fails safely" + _testBoundaryResult boundary-disappeared "disappearance at the probe boundary fails safely" + _testSleepFailure sleep-failure "startup check was interrupted" "interrupted probe restores close-on-exit" + _testSleepFailure sleep-failure-restore-failure "could not restore normal close-on-exit behavior" "interrupted probe reports restore failure" + _testStatusCommands + _testAttachCommands + _testSiblingValidation + _testMissingTmux + _testNames + _testArgumentsAndHelp + _testOutputHelper +} + +function _runSyntaxChecks { + local _bashPath="" + local _file="" + local _version="" + local _syntaxFailed=0 + local _files=("$_target" "$_runner" "$_fakeTmux" "$_fakeSleep") + + for _bashPath in "${_bashPaths[@]}"; do + _version=$("$_bashPath" --version | sed -n '1p') + _syntaxFailed=0 + for _file in "${_files[@]}"; do + if ! "$_bashPath" -n "$_file"; then + _syntaxFailed=1 + fi + done + if [ "$_syntaxFailed" -eq 0 ]; then + _pass "syntax with $_version" + else + _fail "syntax with $_version" + fi + done +} + +function _runShellCheck { + if ! command -v shellcheck >/dev/null 2>&1; then + _fail "ShellCheck is required for '$_mode' (macOS: brew install shellcheck; Ubuntu: apt install shellcheck)" + return + fi + + if shellcheck --shell=bash "$_target" "$_runner" "$_fakeTmux" "$_fakeSleep"; then + _pass "ShellCheck" + else + _fail "ShellCheck" + fi +} + +if [ ! -f "$_target" ] || [ ! -f "$_runner" ] || \ + [ ! -f "$_fakeTmux" ] || [ ! -x "$_fakeTmux" ] || \ + [ ! -f "$_fakeSleep" ] || [ ! -x "$_fakeSleep" ]; then + printf '%s\n' "Test suite files are incomplete." >&2 + exit 2 +fi + +if [ "$(PATH="$_suiteDir/fake-bin:$_originalPath" command -v tmux 2>/dev/null)" != "$_fakeTmux" ] || \ + [ "$(PATH="$_suiteDir/fake-bin:$_originalPath" command -v sleep 2>/dev/null)" != "$_fakeSleep" ]; then + printf '%s\n' "The test fakes do not take precedence in PATH; refusing to run." >&2 + exit 2 +fi + +_discoverBashes +if [ "${#_bashPaths[@]}" -eq 0 ]; then + printf '%s\n' "No Bash interpreter was found." >&2 + exit 2 +fi + +if [ "$_mode" != lint ]; then + if [ "$EUID" -eq 0 ]; then + printf '%s\n' "Behavior tests must not run as root because the wrapper intentionally rejects root." >&2 + exit 2 + fi + + _tempRoot=$(mktemp -d "${TMPDIR:-/tmp}/1mb-start-tests.XXXXXX") || exit 2 + _tempRoot=$(cd -P -- "$_tempRoot" >/dev/null 2>&1 && pwd -P) || exit 2 + _runSyntaxChecks + for _bashUnderTest in "${_bashPaths[@]}"; do + _runBehaviorForBash "$_bashUnderTest" + done +fi + +if [ "$_mode" != test ]; then + _runShellCheck +fi + +printf '\nSummary: %s passed, %s failed, %s skipped\n' "$_passed" "$_failed" "$_skipped" +[ "$_failed" -eq 0 ]