Volume XXII, number 280Wednesday, October 7, 2026Latest message 1 hour ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

v2, 12 partst: prepare `test_match_signal ()` calls for `set -e`

15 messages between Apr 15, 2026 and Apr 16, 2026, from Patrick Steinhardt, Jeff King.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Patrick SteinhardtApr 15, 2026, 13:06 UTC on lore

[PATCH v2 00/12] t: detect errors outside of test cases

Hi,

this is a follow-up to the recent discussion we had around `set -e` to make our tests more robust and basically supersedes Junio's [1].

I've tested the patches with both Bash and Dash, and all tests are passing on my machine with both of them. CI seems to be happy, as well. But I would expect that this change probably has some fallout, even though I hope that it's generally going to be small and contained.

This series is based on 8c9303b1ff (Merge branch 'jc/no-writev-does-not-work', 2026-04-10).

I've created an MR with GitLab [2] and a PR with GitHub [3] to verify that these changes work on both platforms.

Changes in v2:
  - Use `ret=0; $command || ret=$?` pattern.
  - Restore `echo 0` in SIGPIPE tests.
  - Fix "lib-git-svn.sh" to gracefully handle the case where SVN Perl
    modules aren't installed.
  - Use `|| :` consistently instead of `|| true`.
  - Fix up a couple of tests that fail on FreeBSD 15. The test suite is
    now passing on this system, too.
  - Only enable `set -e` on Bash 5 and newer.
  - Link to v1: https://patch.msgid.link/20260413-b4-pks-tests-with-set-e-v1-0-5b83763a0e84@pks.im
Thanks!
Patrick

[1]: <20260325062114.2067946-1-gitster@pobox.com> [2]: https://gitlab.com/gitlab-org/git/-/merge_requests/541 [3]: https://github.com/git/git/pull/2270

---
Patrick Steinhardt (12):
      t: prepare `test_match_signal ()` calls for `set -e`
      t: prepare `test_must_fail ()` for `set -e`
      t: prepare `stop_git_daemon ()` for `set -e`
      t: prepare `git config --unset` calls for `set -e`
      t: prepare conditional test execution for `set -e`
      t: prepare execution of potentially failing commands for `set -e`
      t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`
      t0008: silence error in subshell when using `grep -v`
      t1301: don't fail in case setfacl(1) doesn't exist or fails
      t6002: fix use of `expr` with `set -e`
      t9902: fix use of `read` with `set -e`
      t: detect errors outside of test cases
 t/lib-git-daemon.sh                |  8 +++++---
 t/lib-git-svn.sh                   |  7 +++----
 t/lib-httpd.sh                     |  3 +--
 t/t0005-signals.sh                 |  4 ++--
 t/t0008-ignores.sh                 |  4 ++--
 t/t1301-shared-repo.sh             |  2 +-
 t/t3600-rm.sh                      |  2 +-
 t/t3901-i18n-patch.sh              |  3 ++-
 t/t4032-diff-inter-hunk-context.sh | 14 ++++++++------
 t/t5000-tar-tree.sh                |  4 ++--
 t/t6002-rev-list-bisect.sh         | 17 ++++++++++-------
 t/t7422-submodule-output.sh        |  2 +-
 t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------
 t/t7508-status.sh                  |  4 ++--
 t/t9138-git-svn-authors-prog.sh    |  4 ++--
 t/t9200-git-cvsexportcommit.sh     |  3 +--
 t/t9400-git-cvsserver-server.sh    |  5 +++--
 t/t9401-git-cvsserver-crlf.sh      |  4 ++--
 t/t9402-git-cvsserver-refs.sh      |  4 ++--
 t/t9902-completion.sh              |  2 +-
 t/test-lib-functions.sh            | 12 ++++++------
 t/test-lib.sh                      | 19 +++++++++++++++----
 22 files changed, 85 insertions(+), 66 deletions(-)
Range-diff versus v1:
 1:  210ccb018c !  1:  6e3147dbb1 t: prepare `test_match_signal ()` calls for `set -e`
    @@ Commit message
         but as we expect `foo` to fail this will cause the overall subshell to
         fail once we `set -e`.
     
    -    Fix this issue by using `foo || echo $?` instead.
    +    Fix this issue by using `foo && echo 0 || echo $?` instead.
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
    @@ t/t0005-signals.sh: test_expect_success 'create blob' '
      
      test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '
     -	OUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&
    -+	OUT=$( ((large_git || echo $? 1>&3) | :) 3>&1 ) &&
    ++	OUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
      	test_match_signal 13 "$OUT"
      '
      
      test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '
     -	OUT=$( ((trap "" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&
    -+	OUT=$( ((trap "" PIPE && large_git || echo $? 1>&3) | :) 3>&1 ) &&
    ++	OUT=$( ((trap "" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
      	test_match_signal 13 "$OUT"
      '
      
    @@ t/t3600-rm.sh: test_expect_success 'choking "git rm" should not let it die with
      test_expect_success !MINGW 'choking "git rm" should not let it die with cruft (induce and check SIGPIPE)' '
      	choke_git_rm_setup &&
     -	OUT=$( ((trap "" PIPE && git rm -n "some-file-*"; echo $? 1>&3) | :) 3>&1 ) &&
    -+	OUT=$( ((trap "" PIPE && git rm -n "some-file-*" || echo $? 1>&3) | :) 3>&1 ) &&
    ++	OUT=$( ((trap "" PIPE && git rm -n "some-file-*" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
      	test_match_signal 13 "$OUT" &&
      	test_path_is_missing .git/index.lock
      '
 2:  c056357f6d <  -:  ---------- t: prepare `test_must_fail ()` for `set -e`
 -:  ---------- >  2:  393374871a t: prepare `test_must_fail ()` for `set -e`
 3:  d9076a67ba !  3:  2ff2e3fb7d t: prepare `stop_git_daemon ()` for `set -e`
    @@ Commit message
             than not that we have already killed it, and the call to kill will
             fail.
     
    -    Prepare for this change by making the call to `wait` part of a condition
    -    and by silencing failures of the second call to `kill`.
    +    Prepare for this change by handling the failure of `wait` with `||` and
    +    by silencing failures of the second call to `kill`.
     
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
    @@ t/lib-git-daemon.sh: stop_git_daemon() {
      	kill "$GIT_DAEMON_PID"
     -	wait "$GIT_DAEMON_PID" >&3 2>&4
     -	ret=$?
    -+	if wait "$GIT_DAEMON_PID" >&3 2>&4
    -+	then
    -+		ret=0
    -+	else
    -+		ret=$?
    -+	fi
    ++	ret=0; wait "$GIT_DAEMON_PID" >&3 2>&4 || ret=$?
     +
      	if ! test_match_signal 15 $ret
      	then
 4:  50be774536 =  4:  2c51b9d9fa t: prepare `git config --unset` calls for `set -e`
 5:  b1ac21d4dd =  5:  adba2b830f t: prepare conditional test execution for `set -e`
 6:  19518eeac5 !  6:  61f949e1fb t: prepare execution of potentially failing commands for `set -e`
    @@ t/lib-git-svn.sh: GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn
      then
      	skip_all='skipping git svn tests, svn not found'
      	test_done
    +@@ t/lib-git-svn.sh: export svnrepo
    + svnconf=$PWD/svnconf
    + export svnconf
    + 
    ++x=0
    + perl -w -e "
    + use SVN::Core;
    + use SVN::Repos;
    + \$SVN::Core::VERSION gt '1.1.0' or exit(42);
    + system(qw/svnadmin create --fs-type fsfs/, \$ENV{svnrepo}) == 0 or exit(41);
    +-" >&3 2>&4
    +-x=$?
    ++" >&3 2>&4 || x=$?
    + if test $x -ne 0
    + then
    + 	if test $x -eq 42; then
     
      ## t/lib-httpd.sh ##
     @@ t/lib-httpd.sh: start_httpd() {
    @@ t/lib-httpd.sh: start_httpd() {
      		cat "$HTTPD_ROOT_PATH"/error.log >&4 2>/dev/null
      		test_skip_or_die GIT_TEST_HTTPD "web server setup failed"
     
    + ## t/t3901-i18n-patch.sh ##
    +@@ t/t3901-i18n-patch.sh: check_encoding () {
    + 		8859)
    + 			grep "^encoding ISO8859-1" ;;
    + 		*)
    +-			grep "^encoding ISO8859-1"; test "$?" != 0 ;;
    ++			ret=0; grep "^encoding ISO8859-1" || ret=$?
    ++			test "$ret" != 0 ;;
    + 		esac || return 1
    + 		j=$i
    + 		i=$(($i+1))
    +
    + ## t/t5000-tar-tree.sh ##
    +@@ t/t5000-tar-tree.sh: test_expect_success LONG_IS_64BIT 'set up repository with huge blob' '
    + # would generate the whole 64GB).
    + test_expect_success LONG_IS_64BIT 'generate tar with huge size' '
    + 	{
    +-		git archive HEAD
    +-		echo $? >exit-code
    ++		{ ret=0 && git archive HEAD || ret=$?; } &&
    ++		echo "$ret" >exit-code
    + 	} | test_copy_bytes 4096 >huge.tar &&
    + 	echo 141 >expect &&
    + 	test_cmp expect exit-code
    +
    + ## t/t7422-submodule-output.sh ##
    +@@ t/t7422-submodule-output.sh: test_expect_success !MINGW 'git submodule status --recursive propagates SIGPIPE'
    + 	(
    + 		cd repo &&
    + 		GIT_ALLOW_PROTOCOL=file git submodule add "$(pwd)"/../submodule &&
    +-		{ git submodule status --recursive 2>err; echo $?>status; } |
    ++		{ { ret=0 && git submodule status --recursive 2>err || ret=$?; } && echo $ret >status; } |
    + 			grep -q recursive-submodule-path-1 &&
    + 		test_must_be_empty err &&
    + 		test_match_signal 13 "$(cat status)"
    +
      ## t/t9200-git-cvsexportcommit.sh ##
     @@ t/t9200-git-cvsexportcommit.sh: if ! test_have_prereq PERL; then
      	test_done
    @@ t/t9402-git-cvsserver-refs.sh: check_diff() {
      then
      	skip_all='skipping git-cvsserver tests, perl not available'
     
    + ## t/test-lib-functions.sh ##
    +@@ t/test-lib-functions.sh: test_might_fail () {
    + test_expect_code () {
    + 	want_code=$1
    + 	shift
    +-	"$@" 2>&7
    +-	exit_code=$?
    ++	exit_code=0; "$@" 2>&7 || exit_code=$?
    + 	if test $exit_code = $want_code
    + 	then
    + 		return 0
    +
      ## t/test-lib.sh ##
     @@ t/test-lib.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
      ################################################################
    @@ t/test-lib.sh: export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
      then
      	if test -n "$GIT_TEST_INSTALLED"
      	then
    +@@ t/test-lib.sh: then
    + 	# from any previous runs.
    + 	>"$GIT_TEST_TEE_OUTPUT_FILE"
    + 
    +-	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
    +-	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
    ++	(
    ++		ret=0 && GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1 || ret=$?
    ++		echo "$ret" >"$TEST_RESULTS_BASE.exit"
    ++	) | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
    + 	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
    + 	exit
    + fi
 7:  7d7583d1ea =  7:  697830e576 t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`
 8:  749a350716 !  8:  d5d1ea03ab t0008: silence error in subshell when using `grep -v`
    @@ t/t0008-ignores.sh: test_expect_success_multiple () {
      
     -	expect_verbose=$( echo "$expect_all" | grep -v '^::	' )
     -	expect=$( echo "$expect_verbose" | sed -e 's/.*	//' )
    -+	expect_verbose=$(echo "$expect_all" | grep -v '^::	' || true)
    ++	expect_verbose=$(echo "$expect_all" | grep -v '^::	' || :)
     +	expect=$(echo "$expect_verbose" | sed -e 's/.*	//')
      
      	test_expect_success $prereq "$testname${no_index_opt:+ with $no_index_opt}" '
 9:  14c8dd5148 !  9:  75a150e2dd t1301: don't fail in case setfacl(1) doesn't exist or fails
    @@ t/t1301-shared-repo.sh: TEST_CREATE_REPO_NO_TEMPLATE=1
      
      # Remove a default ACL from the test dir if possible.
     -setfacl -k . 2>/dev/null
    -+setfacl -k . 2>/dev/null || true
    ++setfacl -k . 2>/dev/null || :
      
      # User must have read permissions to the repo -> failure on --shared=0400
      test_expect_success 'shared = 0400 (faulty permission u-w)' '
10:  a81e602616 = 10:  ba22bab22d t6002: fix use of `expr` with `set -e`
11:  dcf5c849e9 = 11:  5a8e2df836 t9902: fix use of `read` with `set -e`
12:  691e1c9b58 ! 12:  8266ee6035 t: detect errors outside of test cases
    @@ Commit message
         Improve the status quo by enabling the errexit option so that any such
         unchecked failures will cause us to abort immediately.
     
    +    Note that for now, we only enable this option for Bash 5 and newer. This
    +    is because other shells have wildly different behaviour, and older
    +    versions of Bash (especially on macOS) are buggy. The list of enabled
    +    shells may be extended going forward.
    +
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
      ## t/test-lib.sh ##
    @@ t/test-lib.sh
      # along with this program.  If not, see https://www.gnu.org/licenses/ .
      
     +# Enable the use of errexit so that any unexpected failures will cause us to
    -+# abort tests, even when outside of a specific test case.
    -+set -e
    ++# abort tests, even when outside of a specific test case. Note that we only
    ++# enable this on Bash 5 and newer, as `set -e` has wildly different behaviour
    ++# across shells. The list of allowed shells may be extended going forward.
    ++if test "${BASH_VERSINFO:=0}" -ge 5
    ++then
    ++	set -e
    ++fi
     +
      # Test the binaries we have just built.  The tests are kept in
      # t/ subdirectory and are run in 'trash directory' subdirectory.

--- base-commit: 8c9303b1ffae5b745d1b0a1f98330cf7944d8db0 change-id: 20260410-b4-pks-tests-with-set-e-3ae479b24b51

Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

We have a couple of calls to `test_match_signal ()` where we execute a Git command and expect it to die with a specific signal. These calls will essentially execute the process in a subshell via `foo; echo $?`, but as we expect `foo` to fail this will cause the overall subshell to fail once we `set -e`.

Fix this issue by using `foo && echo 0 || echo $?` instead.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t0005-signals.sh | 4 ++--
 t/t3600-rm.sh      | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)
Show changes to 2 files +3 −3

t/t0005-signals.sh, t/t3600-rm.sh

diff --git a/t/t0005-signals.sh b/t/t0005-signals.sh
index afba0fc3fc..84319cf169 100755
--- a/t/t0005-signals.sh
+++ b/t/t0005-signals.sh
@@ -42,12 +42,12 @@ test_expect_success 'create blob' '
 '
 
 test_expect_success !MINGW 'a constipated git dies with SIGPIPE' '
-	OUT=$( ((large_git; echo $? 1>&3) | :) 3>&1 ) &&
+	OUT=$( ((large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
 	test_match_signal 13 "$OUT"
 '
 
 test_expect_success !MINGW 'a constipated git dies with SIGPIPE even if parent ignores it' '
-	OUT=$( ((trap "" PIPE && large_git; echo $? 1>&3) | :) 3>&1 ) &&
+	OUT=$( ((trap "" PIPE && large_git && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
 	test_match_signal 13 "$OUT"
 '
 
diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh
index 1f16e6b522..a371ea690e 100755
--- a/t/t3600-rm.sh
+++ b/t/t3600-rm.sh
@@ -260,7 +260,7 @@ test_expect_success 'choking "git rm" should not let it die with cruft (induce S
 
 test_expect_success !MINGW 'choking "git rm" should not let it die with cruft (induce and check SIGPIPE)' '
 	choke_git_rm_setup &&
-	OUT=$( ((trap "" PIPE && git rm -n "some-file-*"; echo $? 1>&3) | :) 3>&1 ) &&
+	OUT=$( ((trap "" PIPE && git rm -n "some-file-*" && echo 0 1>&3 || echo $? 1>&3) | :) 3>&1 ) &&
 	test_match_signal 13 "$OUT" &&
 	test_path_is_missing .git/index.lock
 '
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 02/12] t: prepare `test_must_fail ()` for `set -e`

The helper function `test_must_fail ()` executes a specific Git command that may or may not fail in a specific way. This is done by executing the command in question and then comparing its exit code against a set of conditions.

This works, but once we run our test suite with `set -e` we may bail out of `test_must_fail ()` early in case the command actually fails, even though we expect it to fail. Prepare for this change by handling the failed case with `||`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/test-lib-functions.sh | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
Show changes to t/test-lib-functions.sh +3 −2
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index f3af10fb7e..5fd5494ef1 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1195,8 +1195,9 @@ test_must_fail () {
 		echo >&7 "test_must_fail: only 'git' is allowed: $*"
 		return 1
 	fi
-	"$@" 2>&7
-	exit_code=$?
+
+	exit_code=0; "$@" 2>&7 || exit_code=$?
+
 	if test $exit_code -eq 0 && ! list_contains "$_test_ok" success
 	then
 		echo >&4 "test_must_fail: command succeeded: $*"
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 03/12] t: prepare `stop_git_daemon ()` for `set -e`

We have a couple of calls to `stop_git_daemon ()` outside of specific test cases that will kill a backgrounded git-daemon(1) process and expect the process with a specific error code. While these function calls do end up killing git-daemon(1), the error handling we have in those contexts is basically ineffective. So while we expect the process to exit with a specific error code, we will just continue with any error in case it doesn't.

This will change once we enable `set -e` in a subsequent commit. There's two issues though that will make this _always_ fail:

  - Our call to `wait` is expected to fail, but because it's not part of
    a condition it will cause us to bail out immediately with `set -e`.
  - We try to kill git-daemon(1) a second time via the pidfile. We can
    generally expect that this is the same PID though as we had in the
    "GIT_DAEMON_PID" environment variable, and thus it's more likely
    than not that we have already killed it, and the call to kill will
    fail.

Prepare for this change by handling the failure of `wait` with `||` and by silencing failures of the second call to `kill`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/lib-git-daemon.sh | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
Show changes to t/lib-git-daemon.sh +5 −3
diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh
index e62569222b..d172aa51f0 100644
--- a/t/lib-git-daemon.sh
+++ b/t/lib-git-daemon.sh
@@ -85,14 +85,16 @@ stop_git_daemon() {
 
 	# kill git-daemon child of git
 	say >&3 "Stopping git daemon ..."
+
 	kill "$GIT_DAEMON_PID"
-	wait "$GIT_DAEMON_PID" >&3 2>&4
-	ret=$?
+	ret=0; wait "$GIT_DAEMON_PID" >&3 2>&4 || ret=$?
+
 	if ! test_match_signal 15 $ret
 	then
 		error "git daemon exited with status: $ret"
 	fi
-	kill "$(cat "$GIT_DAEMON_PIDFILE")" 2>/dev/null
+
+	kill "$(cat "$GIT_DAEMON_PIDFILE")" 2>/dev/null || :
 	GIT_DAEMON_PID=
 	rm -f git_daemon_output "$GIT_DAEMON_PIDFILE"
 }
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 04/12] t: prepare `git config --unset` calls for `set -e`

We have a couple of calls to `git config --unset` that ultimately end up as no-ops as the configuration variables aren't set (anymore) in the first place. These calls are mostly intended to recover unconditionally from tests that may have executed only partially, but they'll ultimately fail during a normal test run.

This hasn't been a problem until now as we aren't running tests with `set -e`. This is about to change though, so let's silence the case where we cannot unset the config keys.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t4032-diff-inter-hunk-context.sh | 2 +-
 t/t7508-status.sh                  | 4 ++--
 t/t9138-git-svn-authors-prog.sh    | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)
Show changes to 3 files +5 −5

t/t4032-diff-inter-hunk-context.sh, t/t7508-status.sh, t/t9138-git-svn-authors-prog.sh

diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh
index bada0cbd32..c98eb6abb2 100755
--- a/t/t4032-diff-inter-hunk-context.sh
+++ b/t/t4032-diff-inter-hunk-context.sh
@@ -17,7 +17,7 @@ f() {
 
 t() {
 	use_config=
-	git config --unset diff.interHunkContext
+	git config --unset diff.interHunkContext || :
 
 	case $# in
 	4) hunks=$4; cmd="diff -U$3";;
diff --git a/t/t7508-status.sh b/t/t7508-status.sh
index a5e21bf8bf..1167b835a4 100755
--- a/t/t7508-status.sh
+++ b/t/t7508-status.sh
@@ -773,8 +773,8 @@ test_expect_success TTY 'status --porcelain ignores color.status' '
 '
 
 # recover unconditionally from color tests
-git config --unset color.status
-git config --unset color.ui
+git config --unset color.status || :
+git config --unset color.ui || :
 
 test_expect_success 'status --porcelain respects -b' '
 
diff --git a/t/t9138-git-svn-authors-prog.sh b/t/t9138-git-svn-authors-prog.sh
index 784ec7fc2d..5bb38cb23a 100755
--- a/t/t9138-git-svn-authors-prog.sh
+++ b/t/t9138-git-svn-authors-prog.sh
@@ -68,8 +68,8 @@ test_expect_success 'authors-file overrode authors-prog' '
 	)
 '
 
-git --git-dir=x/.git config --unset svn.authorsfile
-git --git-dir=x/.git config --unset svn.authorsprog
+git --git-dir=x/.git config --unset svn.authorsfile || :
+git --git-dir=x/.git config --unset svn.authorsprog || :
 
 test_expect_success 'authors-prog imported user without email' '
 	svn mkdir -m gg --username gg-hermit "$svnrepo"/gg &&
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 05/12] t: prepare conditional test execution for `set -e`

We have some test in our test suite where we use the pattern of `test ... && test_expect_succeess` to conditionally execute a test. The problem is that when we decide to not execute the test, we'll indeed skip the test, but the overall statement will also be unsuccessful. This will become a problem once we enable `set -e`.

Prepare for this future by turning this into a proper conditional, which is also a bit easier to read overall.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t4032-diff-inter-hunk-context.sh | 12 +++++++-----
 t/t7450-bad-git-dotfiles.sh        | 24 +++++++++++++-----------
 2 files changed, 20 insertions(+), 16 deletions(-)
Show changes to 2 files +20 −16

t/t4032-diff-inter-hunk-context.sh, t/t7450-bad-git-dotfiles.sh

diff --git a/t/t4032-diff-inter-hunk-context.sh b/t/t4032-diff-inter-hunk-context.sh
index c98eb6abb2..2d216fb70f 100755
--- a/t/t4032-diff-inter-hunk-context.sh
+++ b/t/t4032-diff-inter-hunk-context.sh
@@ -40,11 +40,13 @@ t() {
 		test $(git $cmd $file | grep '^@@ ' | wc -l) = $hunks
 	"
 
-	test -f $expected &&
-	test_expect_success "$label: check output" "
-		git $cmd $file | grep -v '^index ' >actual &&
-		test_cmp $expected actual
-	"
+	if test -f $expected
+	then
+		test_expect_success "$label: check output" "
+			git $cmd $file | grep -v '^index ' >actual &&
+			test_cmp $expected actual
+		"
+	fi
 }
 
 cat <<EOF >expected.f1.0.1 || exit 1
diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh
index f512eed278..8cc86522b2 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -220,17 +220,19 @@ check_dotx_symlink () {
 		)
 	'
 
-	test -n "$refuse_index" &&
-	test_expect_success "refuse to load symlinked $name into index ($type)" '
-		test_must_fail \
-			git -C $dir \
-			    -c core.protectntfs \
-			    -c core.protecthfs \
-			    read-tree $tree 2>err &&
-		grep "invalid path.*$name" err &&
-		git -C $dir ls-files -s >out &&
-		test_must_be_empty out
-	'
+	if test -n "$refuse_index"
+	then
+		test_expect_success "refuse to load symlinked $name into index ($type)" '
+			test_must_fail \
+				git -C $dir \
+				    -c core.protectntfs \
+				    -c core.protecthfs \
+				    read-tree $tree 2>err &&
+			grep "invalid path.*$name" err &&
+			git -C $dir ls-files -s >out &&
+			test_must_be_empty out
+		'
+	fi
 }
 
 check_dotx_symlink gitmodules vanilla .gitmodules
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 06/12] t: prepare execution of potentially failing commands for `set -e`

Several of our tests verify whether a certain binary can be executed, potentially skipping tests in case we cannot, for example because the binary doesn't exist. In those cases we often run the binary outside of any conditionally.

This will start to fail once we enable `set -e`, as that will cause us to bail out the test immediately. Improve these tests by executing them inside of a conditional instead.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/lib-git-svn.sh                |  7 +++----
 t/lib-httpd.sh                  |  3 +--
 t/t3901-i18n-patch.sh           |  3 ++-
 t/t5000-tar-tree.sh             |  4 ++--
 t/t7422-submodule-output.sh     |  2 +-
 t/t9200-git-cvsexportcommit.sh  |  3 +--
 t/t9400-git-cvsserver-server.sh |  5 +++--
 t/t9401-git-cvsserver-crlf.sh   |  4 ++--
 t/t9402-git-cvsserver-refs.sh   |  4 ++--
 t/test-lib-functions.sh         |  3 +--
 t/test-lib.sh                   | 10 ++++++----
 11 files changed, 24 insertions(+), 24 deletions(-)
Show changes to 11 files +24 −24

t/lib-git-svn.sh, t/lib-httpd.sh, t/t3901-i18n-patch.sh, t/t5000-tar-tree.sh, t/t7422-submodule-output.sh, t/t9200-git-cvsexportcommit.sh, t/t9400-git-cvsserver-server.sh, t/t9401-git-cvsserver-crlf.sh, t/t9402-git-cvsserver-refs.sh, t/test-lib-functions.sh, t/test-lib.sh

diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
index 2fde2353fd..52843f667d 100644
--- a/t/lib-git-svn.sh
+++ b/t/lib-git-svn.sh
@@ -15,8 +15,7 @@ GIT_SVN_DIR=$GIT_DIR/svn/refs/remotes/git-svn
 SVN_TREE=$GIT_SVN_DIR/svn-tree
 test_set_port SVNSERVE_PORT
 
-svn >/dev/null 2>&1
-if test $? -ne 1
+if ! svn help >/dev/null 2>&1
 then
 	skip_all='skipping git svn tests, svn not found'
 	test_done
@@ -27,13 +26,13 @@ export svnrepo
 svnconf=$PWD/svnconf
 export svnconf
 
+x=0
 perl -w -e "
 use SVN::Core;
 use SVN::Repos;
 \$SVN::Core::VERSION gt '1.1.0' or exit(42);
 system(qw/svnadmin create --fs-type fsfs/, \$ENV{svnrepo}) == 0 or exit(41);
-" >&3 2>&4
-x=$?
+" >&3 2>&4 || x=$?
 if test $x -ne 0
 then
 	if test $x -eq 42; then
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index 4c76e813e3..fc646447d5 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -235,11 +235,10 @@ start_httpd() {
 
 	test_atexit stop_httpd
 
-	"$LIB_HTTPD_PATH" -d "$HTTPD_ROOT_PATH" \
+	if ! "$LIB_HTTPD_PATH" -d "$HTTPD_ROOT_PATH" \
 		-f "$TEST_PATH/apache.conf" $HTTPD_PARA \
 		-c "Listen 127.0.0.1:$LIB_HTTPD_PORT" -k start \
 		>&3 2>&4
-	if test $? -ne 0
 	then
 		cat "$HTTPD_ROOT_PATH"/error.log >&4 2>/dev/null
 		test_skip_or_die GIT_TEST_HTTPD "web server setup failed"
diff --git a/t/t3901-i18n-patch.sh b/t/t3901-i18n-patch.sh
index f03601b49a..ef7d7e1edc 100755
--- a/t/t3901-i18n-patch.sh
+++ b/t/t3901-i18n-patch.sh
@@ -28,7 +28,8 @@ check_encoding () {
 		8859)
 			grep "^encoding ISO8859-1" ;;
 		*)
-			grep "^encoding ISO8859-1"; test "$?" != 0 ;;
+			ret=0; grep "^encoding ISO8859-1" || ret=$?
+			test "$ret" != 0 ;;
 		esac || return 1
 		j=$i
 		i=$(($i+1))
diff --git a/t/t5000-tar-tree.sh b/t/t5000-tar-tree.sh
index 5465054f17..a8c28533dc 100755
--- a/t/t5000-tar-tree.sh
+++ b/t/t5000-tar-tree.sh
@@ -503,8 +503,8 @@ test_expect_success LONG_IS_64BIT 'set up repository with huge blob' '
 # would generate the whole 64GB).
 test_expect_success LONG_IS_64BIT 'generate tar with huge size' '
 	{
-		git archive HEAD
-		echo $? >exit-code
+		{ ret=0 && git archive HEAD || ret=$?; } &&
+		echo "$ret" >exit-code
 	} | test_copy_bytes 4096 >huge.tar &&
 	echo 141 >expect &&
 	test_cmp expect exit-code
diff --git a/t/t7422-submodule-output.sh b/t/t7422-submodule-output.sh
index aea1ddf117..852136fdfd 100755
--- a/t/t7422-submodule-output.sh
+++ b/t/t7422-submodule-output.sh
@@ -198,7 +198,7 @@ test_expect_success !MINGW 'git submodule status --recursive propagates SIGPIPE'
 	(
 		cd repo &&
 		GIT_ALLOW_PROTOCOL=file git submodule add "$(pwd)"/../submodule &&
-		{ git submodule status --recursive 2>err; echo $?>status; } |
+		{ { ret=0 && git submodule status --recursive 2>err || ret=$?; } && echo $ret >status; } |
 			grep -q recursive-submodule-path-1 &&
 		test_must_be_empty err &&
 		test_match_signal 13 "$(cat status)"
diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh
index 14cbe96527..581cf3d28f 100755
--- a/t/t9200-git-cvsexportcommit.sh
+++ b/t/t9200-git-cvsexportcommit.sh
@@ -11,8 +11,7 @@ if ! test_have_prereq PERL; then
 	test_done
 fi
 
-cvs >/dev/null 2>&1
-if test $? -ne 1
+if ! cvs version >/dev/null 2>&1
 then
     skip_all='skipping git cvsexportcommit tests, cvs not found'
     test_done
diff --git a/t/t9400-git-cvsserver-server.sh b/t/t9400-git-cvsserver-server.sh
index e499c7f955..4b45398bab 100755
--- a/t/t9400-git-cvsserver-server.sh
+++ b/t/t9400-git-cvsserver-server.sh
@@ -17,12 +17,13 @@ if ! test_have_prereq PERL; then
 	skip_all='skipping git cvsserver tests, perl not available'
 	test_done
 fi
-cvs >/dev/null 2>&1
-if test $? -ne 1
+
+if ! cvs version >/dev/null 2>&1
 then
     skip_all='skipping git-cvsserver tests, cvs not found'
     test_done
 fi
+
 perl -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {
     skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'
     test_done
diff --git a/t/t9401-git-cvsserver-crlf.sh b/t/t9401-git-cvsserver-crlf.sh
index a34805acdc..6b4cbb1651 100755
--- a/t/t9401-git-cvsserver-crlf.sh
+++ b/t/t9401-git-cvsserver-crlf.sh
@@ -60,12 +60,12 @@ check_status_options() {
     return $stat
 }
 
-cvs >/dev/null 2>&1
-if test $? -ne 1
+if ! cvs version >/dev/null 2>&1
 then
     skip_all='skipping git-cvsserver tests, cvs not found'
     test_done
 fi
+
 if ! test_have_prereq PERL
 then
     skip_all='skipping git-cvsserver tests, perl not available'
diff --git a/t/t9402-git-cvsserver-refs.sh b/t/t9402-git-cvsserver-refs.sh
index 2ee41f9443..65f2ceedec 100755
--- a/t/t9402-git-cvsserver-refs.sh
+++ b/t/t9402-git-cvsserver-refs.sh
@@ -68,12 +68,12 @@ check_diff() {
 
 #########
 
-cvs >/dev/null 2>&1
-if test $? -ne 1
+if ! cvs version >/dev/null 2>&1
 then
 	skip_all='skipping git-cvsserver tests, cvs not found'
 	test_done
 fi
+
 if ! test_have_prereq PERL
 then
 	skip_all='skipping git-cvsserver tests, perl not available'
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 5fd5494ef1..879ee1ee59 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1248,8 +1248,7 @@ test_might_fail () {
 test_expect_code () {
 	want_code=$1
 	shift
-	"$@" 2>&7
-	exit_code=$?
+	exit_code=0; "$@" 2>&7 || exit_code=$?
 	if test $exit_code = $want_code
 	then
 		return 0
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 70fd3e9baf..de7d9e7b92 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -143,8 +143,8 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
 ################################################################
 # It appears that people try to run tests without building...
 GIT_BINARY="${GIT_TEST_INSTALLED:-$GIT_BUILD_DIR}/git$X"
-"$GIT_BINARY" >/dev/null
-if test $? != 1
+
+if ! "$GIT_BINARY" version >/dev/null
 then
 	if test -n "$GIT_TEST_INSTALLED"
 	then
@@ -454,8 +454,10 @@ then
 	# from any previous runs.
 	>"$GIT_TEST_TEE_OUTPUT_FILE"
 
-	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
-	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
+	(
+		ret=0 && GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1 || ret=$?
+		echo "$ret" >"$TEST_RESULTS_BASE.exit"
+	) | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
 	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
 	exit
 fi
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 07/12] t: prepare `test_when_finished ()`/`test_atexit()` for `set -e`

Both `test_when_finished ()` and `test_atexit ()` build up a chain of cleanup commands by prepending each new command to the existing cleanup string. To preserve the exit code of the test body across cleanup execution, we append the following logic:

    } && (exit "$eval_ret"); eval_ret=$?; ...

The intent of this is to run the cleanup block and then unconditionally restore `eval_ret`. The original behaviour of this is is:

   +------------------+---------+------------------------------------+
   |test body         │ cleanup │ old behaviour                      │
   +------------------+---------+------------------------------------+
   │pass (eval_ret=0) | pass    │ && taken -> (exit 0) -> eval_ret=0 |
   +------------------+---------+------------------------------------+
   │pass (eval_ret=0) | fail    │ && not taken -> eval_ret=$?        |
   +------------------+---------+------------------------------------+
   │fail (eval_ret=1) | pass    │ && taken -> (exit 1) -> eval_ret=1 |
   +------------------+---------+------------------------------------+
   │fail (eval_ret=1) | fail    | && not taken -> eval_ret=$?        |
   +------------------+---------+------------------------------------+

This logic will start to fail once we enable `set -e`. When `$eval_ret` is non-zero, the subshell we create will fail, and with `set -e` we'll thus bail out without evaluating the logic after the semicolon.

Fix this issue by instead using `|| eval_ret=\$?; ...`. Besides being a bit simpler, it also retains the original behaviour:

   +------------------+---------+------------------------------------+
   |test body         │ cleanup │ old behaviour                      │
   +------------------+---------+------------------------------------+
   │pass (eval_ret=0) | pass    │ || not taken -> eval_ret unchanged |
   +------------------+---------+------------------------------------+
   │pass (eval_ret=0) | fail    │ || taken -> eval_ret=$?            |
   +------------------+---------+------------------------------------+
   │fail (eval_ret=1) | pass    │ || not taken -> eval_ret unchanged |
   +------------------+---------+------------------------------------+
   │fail (eval_ret=1) | fail    | || taken -> eval_ret=$?            |
   +------------------+---------+------------------------------------+
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/test-lib-functions.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to t/test-lib-functions.sh +2 −2
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 879ee1ee59..502bb0ddcb 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1512,7 +1512,7 @@ test_when_finished () {
 	test "${BASH_SUBSHELL-0}" = 0 ||
 	BUG "test_when_finished does nothing in a subshell"
 	test_cleanup="{ $*
-		} && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup"
+		} || eval_ret=\$?; $test_cleanup"
 }
 
 # This function can be used to schedule some commands to be run
@@ -1540,7 +1540,7 @@ test_atexit () {
 	test "${BASH_SUBSHELL-0}" = 0 ||
 	BUG "test_atexit does nothing in a subshell"
 	test_atexit_cleanup="{ $*
-		} && (exit \"\$eval_ret\"); eval_ret=\$?; $test_atexit_cleanup"
+		} || eval_ret=\$?; $test_atexit_cleanup"
 }
 
 # Deprecated wrapper for "git init", use "git init" directly instead
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 08/12] t0008: silence error in subshell when using `grep -v`

In t0008 we use `grep -v` in a subshell, but expect that this command will sometimes not match anything. This would cause grep(1) to return an error code, but given that we don't run with `set -e` we swallow this error.

We're about to enable `set -e`. Prepare for this by ignoring any errors.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t0008-ignores.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to t/t0008-ignores.sh +2 −2
diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh
index e716b5cdfa..d77a179bdd 100755
--- a/t/t0008-ignores.sh
+++ b/t/t0008-ignores.sh
@@ -122,8 +122,8 @@ test_expect_success_multiple () {
 	fi
 	testname="$1" expect_all="$2" code="$3"
 
-	expect_verbose=$( echo "$expect_all" | grep -v '^::	' )
-	expect=$( echo "$expect_verbose" | sed -e 's/.*	//' )
+	expect_verbose=$(echo "$expect_all" | grep -v '^::	' || :)
+	expect=$(echo "$expect_verbose" | sed -e 's/.*	//')
 
 	test_expect_success $prereq "$testname${no_index_opt:+ with $no_index_opt}" '
 		expect "$expect" &&
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 09/12] t1301: don't fail in case setfacl(1) doesn't exist or fails

In t1301 we're trying to remove any potentially-existing default ACLs that might exist on the transh directory by executing setfacl(1). According to 8ed0a740dd (t1301-shared-repo.sh: don't let a default ACL interfere with the test, 2008-10-16), this is done because we play around with permissions and umasks in this test suite.

The setfacl(1) binary may not exist on some systems though, even though tests ultimately still pass. This doesn't matter currently, but will cause the test to fail once we start running with `set -e`. Silence such failures by ignoring failures here.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t1301-shared-repo.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to t/t1301-shared-repo.sh +1 −1
diff --git a/t/t1301-shared-repo.sh b/t/t1301-shared-repo.sh
index 630a47af21..0e0d07a1a1 100755
--- a/t/t1301-shared-repo.sh
+++ b/t/t1301-shared-repo.sh
@@ -12,7 +12,7 @@ TEST_CREATE_REPO_NO_TEMPLATE=1
 . ./test-lib.sh
 
 # Remove a default ACL from the test dir if possible.
-setfacl -k . 2>/dev/null
+setfacl -k . 2>/dev/null || :
 
 # User must have read permissions to the repo -> failure on --shared=0400
 test_expect_success 'shared = 0400 (faulty permission u-w)' '
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 10/12] t6002: fix use of `expr` with `set -e`

In `test_bisection_diff ()` we use `expr` to perform some math. This command has some gotchas though in that it will only return success when the result is neither null nor zero. In some of our cases though it actually _is_ zero, and that will cause the expressions to fail once we enable `set -e`.

Prepare for this change by instead using `$(( ))`, which doesn't have the same issue. While at it, modernize the function a tiny bit.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t6002-rev-list-bisect.sh | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)
Show changes to t/t6002-rev-list-bisect.sh +10 −7
diff --git a/t/t6002-rev-list-bisect.sh b/t/t6002-rev-list-bisect.sh
index daa009c9a1..f2de40b5ed 100755
--- a/t/t6002-rev-list-bisect.sh
+++ b/t/t6002-rev-list-bisect.sh
@@ -27,13 +27,16 @@ test_bisection_diff()
 	# Test if bisection size is close to half of list size within
 	# tolerance.
 	#
-	_bisect_err=$(expr $_list_size - $_bisection_size \* 2)
-	test "$_bisect_err" -lt 0 && _bisect_err=$(expr 0 - $_bisect_err)
-	_bisect_err=$(expr $_bisect_err / 2) ; # floor
-
-	test_expect_success \
-	"bisection diff $_bisect_option $_head $* <= $_max_diff" \
-	'test $_bisect_err -le $_max_diff'
+	_bisect_err=$(($_list_size - $_bisection_size * 2))
+	if test "$_bisect_err" -lt 0
+	then
+		_bisect_err=$((0 - $_bisect_err))
+	fi
+	_bisect_err=$(($_bisect_err / 2)) ; # floor
+
+	test_expect_success "bisection diff $_bisect_option $_head $* <= $_max_diff" '
+		test $_bisect_err -le $_max_diff
+	'
 }
 
 date >path0
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 11/12] t9902: fix use of `read` with `set -e`

In t9902 we're using the `read` builtin to read some values into a variable. This is done by using `-d ""`, which cause us to read until the end of the heredoc. There is a gotcha though: when the delimiter isn't found at all, then the read builtin will return an error. This hasn't been an issue until now as we didn't run with `set -e`, but that'll change in a subsequent commit.

Prepare for this change by silencing the error.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/t9902-completion.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to t/t9902-completion.sh +1 −1
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index 2f9a597ec7..e3a7df7691 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -590,7 +590,7 @@ test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '
 	__gitcomp "$invalid_variable_name"
 '
 
-read -r -d "" refs <<-\EOF
+read -r -d "" refs <<-\EOF || :
 main
 maint
 next
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Patrick SteinhardtApr 15, 2026, 13:06 UTC in reply to Patrick Steinhardt on lore

[PATCH v2 12/12] t: detect errors outside of test cases

We have recently merged a patch series that had a simple misspelling of `test_expect_success`. Instead of making our tests fail though, this typo went completely undetected and all of our tests passed, which is of course unfortunate. This is a more general issue with our test suite: all commands that run outside of a specific test case can fail, and if we don't explicitly check for such failure then this failure will be silently ignored.

Improve the status quo by enabling the errexit option so that any such unchecked failures will cause us to abort immediately.

Note that for now, we only enable this option for Bash 5 and newer. This is because other shells have wildly different behaviour, and older versions of Bash (especially on macOS) are buggy. The list of enabled shells may be extended going forward.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
---
 t/test-lib.sh | 9 +++++++++
 1 file changed, 9 insertions(+)
Show changes to t/test-lib.sh +9 −0
diff --git a/t/test-lib.sh b/t/test-lib.sh
index de7d9e7b92..1f7868c537 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -15,6 +15,15 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see https://www.gnu.org/licenses/ .
 
+# Enable the use of errexit so that any unexpected failures will cause us to
+# abort tests, even when outside of a specific test case. Note that we only
+# enable this on Bash 5 and newer, as `set -e` has wildly different behaviour
+# across shells. The list of allowed shells may be extended going forward.
+if test "${BASH_VERSINFO:=0}" -ge 5
+then
+	set -e
+fi
+
 # Test the binaries we have just built.  The tests are kept in
 # t/ subdirectory and are run in 'trash directory' subdirectory.
 if test -z "$TEST_DIRECTORY"
-- 
2.54.0.rc2.529.gd9106f7525.dirty
Jeff KingApr 16, 2026, 06:00 UTC in reply to Patrick Steinhardt on lore

Re: [PATCH v2 12/12] t: detect errors outside of test cases

On Wed, Apr 15, 2026 at 03:06:45PM +0200, Patrick Steinhardt wrote:
Show 7 quoted lines
> Improve the status quo by enabling the errexit option so that any such
> unchecked failures will cause us to abort immediately.
> 
> Note that for now, we only enable this option for Bash 5 and newer. This
> is because other shells have wildly different behaviour, and older
> versions of Bash (especially on macOS) are buggy. The list of enabled
> shells may be extended going forward.

OK, we know that this does not cause false positives because all of the tests should pass. It would be nice if we could verify that it catches bugs, too. Doing this:

Show changes to t/t0001-init.sh +2 −1
diff --git a/t/t0001-init.sh b/t/t0001-init.sh
index e4d32bb4d2..5521f21e64 100755
--- a/t/t0001-init.sh
+++ b/t/t0001-init.sh
@@ -980,4 +980,6 @@ test_expect_success 're-init reads matching includeIf.onbranch' '
 	test_cmp expect err
 '
 
+test_expect_foobar 'baz'
+
 test_done

will fail for me, but only if I specially ask to use bash, either
manually or by setting TEST_SHELL_PATH (since /bin/sh is dash on
Debian). Is there something in both GitHub and GitLab CI that will
reliably use an acceptable version of bash?

I guess perhaps Windows, though I don't know what version is used there.
But should we maybe set TEST_SHELL_PATH in at least one of the linux
builds?

-Peff
Patrick SteinhardtApr 16, 2026, 10:46 UTC in reply to Jeff King on lore

Re: [PATCH v2 12/12] t: detect errors outside of test cases

On Thu, Apr 16, 2026 at 02:00:59AM -0400, Jeff King wrote:
Show 34 quoted lines
> On Wed, Apr 15, 2026 at 03:06:45PM +0200, Patrick Steinhardt wrote:
> 
> > Improve the status quo by enabling the errexit option so that any such
> > unchecked failures will cause us to abort immediately.
> > 
> > Note that for now, we only enable this option for Bash 5 and newer. This
> > is because other shells have wildly different behaviour, and older
> > versions of Bash (especially on macOS) are buggy. The list of enabled
> > shells may be extended going forward.
> 
> OK, we know that this does not cause false positives because all of the
> tests should pass. It would be nice if we could verify that it catches
> bugs, too. Doing this:
> 
> diff --git a/t/t0001-init.sh b/t/t0001-init.sh
> index e4d32bb4d2..5521f21e64 100755
> --- a/t/t0001-init.sh
> +++ b/t/t0001-init.sh
> @@ -980,4 +980,6 @@ test_expect_success 're-init reads matching includeIf.onbranch' '
>  	test_cmp expect err
>  '
>  
> +test_expect_foobar 'baz'
> +
>  test_done
> 
> will fail for me, but only if I specially ask to use bash, either
> manually or by setting TEST_SHELL_PATH (since /bin/sh is dash on
> Debian). Is there something in both GitHub and GitLab CI that will
> reliably use an acceptable version of bash?
> 
> I guess perhaps Windows, though I don't know what version is used there.
> But should we maybe set TEST_SHELL_PATH in at least one of the linux
> builds?

Our Fedora-based builds use Bash 5.3.0, so we at least have some test coverage [1]. But I agree that it would make sense to maybe also make one of our Ubuntu-based builds use Bash explicitly instead of Dash.

Patrick
[1]: https://gitlab.com/gitlab-org/git/-/jobs/13947942805

Back to recent threads