threads / rfc / 49946

RFC patch, 3 partstest-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

Subject: [RFC PATCH 0/3] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

## tl;dr

67 messages between Dec 4, 2018 and Jan 7, 2019. Diffs are folded; open one to read it.

replies: 66people: 5as markdown or json

SZEDER Gábor· Dec 4, 2018, 16:34 UTC · lore
Inspired by Peff's 'stress' script mentioned in:
  https://public-inbox.org/git/20181122161722.GC28192@sigill.intra.peff.net/

the last patch in this series brings that functionality to our test library to help to reproduce failures in flaky tests. So

  ./t1234-foo --stress
  
will run that test script repeatedly in multiple parallel invocations,
in the hope that the increased load creates enough variance in the
timing of the test's commands that a failure is evenually triggered.
SZEDER Gábor (3):
  test-lib: consolidate naming of test-results paths
  test-lib-functions: introduce the 'test_set_port' helper function
  test-lib: add the '--stress' option to run a test repeatedly under
    load
 t/README                 | 13 +++++-
 t/lib-git-daemon.sh      |  2 +-
 t/lib-git-p4.sh          |  9 +---
 t/lib-git-svn.sh         |  2 +-
 t/lib-httpd.sh           |  2 +-
 t/t0410-partial-clone.sh |  1 -
 t/t5512-ls-remote.sh     |  2 +-
 t/test-lib-functions.sh  | 39 ++++++++++++++++
 t/test-lib.sh            | 99 +++++++++++++++++++++++++++++++++++-----
 9 files changed, 143 insertions(+), 26 deletions(-)
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 4, 2018, 16:34 UTC · re: SZEDER Gábor · lore

[PATCH 1/3] test-lib: consolidate naming of test-results paths

There are two places where we strip off any leading path components and the '.sh' suffix from the test script's pathname, and there are two places where we construct the filename of test output files in 't/test-results/'. The last patch in this series will add even more.

Factor these out into helper variables to avoid repeating ourselves.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)
Show changes to t/test-lib.sh +10 −11
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 0f1faa24b2..49e4563405 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -71,6 +71,9 @@ then
 	exit 1
 fi
 
+TEST_NAME="$(basename "$0" .sh)"
+TEST_RESULTS_BASE="$TEST_OUTPUT_DIRECTORY/test-results/$TEST_NAME"
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 case "$GIT_TEST_TEE_STARTED, $* " in
@@ -78,12 +81,11 @@ done,*)
 	# do not redirect again
 	;;
 *' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
-	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
-	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
+	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
 
 	# Make this filename available to the sub-process in case it is using
 	# --verbose-log.
-	GIT_TEST_TEE_OUTPUT_FILE=$BASE.out
+	GIT_TEST_TEE_OUTPUT_FILE=$TEST_RESULTS_BASE.out
 	export GIT_TEST_TEE_OUTPUT_FILE
 
 	# Truncate before calling "tee -a" to get rid of the results
@@ -91,8 +93,8 @@ done,*)
 	>"$GIT_TEST_TEE_OUTPUT_FILE"
 
 	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
-	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
-	test "$(cat "$BASE.exit")" = 0
+	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
+	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
 	exit
 	;;
 esac
@@ -818,12 +820,9 @@ test_done () {
 
 	if test -z "$HARNESS_ACTIVE"
 	then
-		test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results"
-		mkdir -p "$test_results_dir"
-		base=${0##*/}
-		test_results_path="$test_results_dir/${base%.sh}.counts"
+		mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
 
-		cat >"$test_results_path" <<-EOF
+		cat >"$TEST_RESULTS_BASE.counts" <<-EOF
 		total $test_count
 		success $test_success
 		fixed $test_fixed
@@ -1029,7 +1028,7 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$(basename "$0" .sh)"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Jeff King· Dec 5, 2018, 04:57 UTC · re: SZEDER Gábor · lore

Re: [PATCH 1/3] test-lib: consolidate naming of test-results paths

On Tue, Dec 04, 2018 at 05:34:55PM +0100, SZEDER Gábor wrote:
Show 6 quoted lines
> There are two places where we strip off any leading path components
> and the '.sh' suffix from the test script's pathname, and there are
> two places where we construct the filename of test output files in
> 't/test-results/'.  The last patch in this series will add even more.
> 
> Factor these out into helper variables to avoid repeating ourselves.
Makes sense.
> +TEST_NAME="$(basename "$0" .sh)"
> +TEST_RESULTS_BASE="$TEST_OUTPUT_DIRECTORY/test-results/$TEST_NAME"
Hmm, since we are building up this BASE variable anyway, why not:
  TEST_RESULTS_DIR=$TEST_OUTPUT_DIRECTORY/test-results
  TEST_RESULTS_BASE=$TEST_RESULTS_DIR/$TEST_NAME
? That saves having to run `dirname` on it later.

I guess one could argue that saying "the directory name of the file I'm writing" is more readable. I just generally try to avoid extra manipulation of the strings when possible (especially in shell).

-Peff
SZEDER Gábor· Dec 4, 2018, 16:34 UTC · re: SZEDER Gábor · lore

[PATCH 2/3] test-lib-functions: introduce the 'test_set_port' helper function

Several test scripts run daemons like 'git-daemon' or Apache, and communicate with them through TCP sockets. To have unique ports where these daemons are accessible, the ports are usually the number of the corresponding test scripts, unless the user overrides them via environment variables, and thus all those tests and test libs contain more or less the same bit of one-liner boilerplate code to find out the port. The last patch in this series will make this a bit more complicated.

Factor out finding the port for a daemon into the common helper function 'test_set_port' to avoid repeating ourselves.

Take special care of test scripts with "low" numbers:
  - Test numbers below 1024 would result in a port that's only usable
    as root, so set their port to '10000 + test-nr' to make sure it
    doesn't interfere with other tests in the test suite.  This makes
    the hardcoded port number in 't0410-partial-clone.sh' unnecessary,
    remove it.
  - The shell's arithmetic evaluation interprets numbers with leading
    zeros as octal values, which means that test number below 1000 and
    containing the digits 8 or 9 will trigger an error.  Remove all
    leading zeros from the test numbers to prevent this.

Note that the Perforce tests are unlike the other tests involving daemons in that:

  - 'lib-git-p4.sh' doesn't use the test's number for unique port as
    is, but does a bit of additional arithmetic on top [1].
  - The port is not overridable via an environment variable.

With this patch even Perforce tests will use the test's number as default port, and it will be overridable via the P4DPORT environment variable.

[1] Commit fc00233071 (git-p4 tests: refactor and cleanup, 2011-08-22)
    introduced that "unusual" unique port computation without
    explaining why it was necessary (as opposed to simply using the
    test number as is).  It seems to be just unnecessary complication,
    and in any case that commit came way before the "test nr as unique
    port" got "standardized" for other daemons in commits c44132fcf3
    (tests: auto-set git-daemon port, 2014-02-10), 3bb486e439 (tests:
    auto-set LIB_HTTPD_PORT from test name, 2014-02-10), and
    bf9d7df950 (t/lib-git-svn.sh: improve svnserve tests with parallel
    make test, 2017-12-01).
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/lib-git-daemon.sh      |  2 +-
 t/lib-git-p4.sh          |  9 +--------
 t/lib-git-svn.sh         |  2 +-
 t/lib-httpd.sh           |  2 +-
 t/t0410-partial-clone.sh |  1 -
 t/t5512-ls-remote.sh     |  2 +-
 t/test-lib-functions.sh  | 36 ++++++++++++++++++++++++++++++++++++
 7 files changed, 41 insertions(+), 13 deletions(-)
Show changes to 7 files +41 −13

t/lib-git-daemon.sh, t/lib-git-p4.sh, t/lib-git-svn.sh, t/lib-httpd.sh, t/t0410-partial-clone.sh, t/t5512-ls-remote.sh, t/test-lib-functions.sh

diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh
index f98de95c15..41eb1e3ae8 100644
--- a/t/lib-git-daemon.sh
+++ b/t/lib-git-daemon.sh
@@ -28,7 +28,7 @@ then
 	test_skip_or_die $GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
 fi
 
-LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-${this_test#t}}
+test_set_port LIB_GIT_DAEMON_PORT
 
 GIT_DAEMON_PID=
 GIT_DAEMON_DOCUMENT_ROOT_PATH="$PWD"/repo
diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh
index c27599474c..b3be3ba011 100644
--- a/t/lib-git-p4.sh
+++ b/t/lib-git-p4.sh
@@ -53,14 +53,7 @@ time_in_seconds () {
 	(cd / && "$PYTHON_PATH" -c 'import time; print(int(time.time()))')
 }
 
-# Try to pick a unique port: guess a large number, then hope
-# no more than one of each test is running.
-#
-# This does not handle the case where somebody else is running the
-# same tests and has chosen the same ports.
-testid=${this_test#t}
-git_p4_test_start=9800
-P4DPORT=$((10669 + ($testid - $git_p4_test_start)))
+test_set_port P4DPORT
 
 P4PORT=localhost:$P4DPORT
 P4CLIENT=client
diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
index a8130f9119..f3b478c307 100644
--- a/t/lib-git-svn.sh
+++ b/t/lib-git-svn.sh
@@ -13,6 +13,7 @@ fi
 GIT_DIR=$PWD/.git
 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
@@ -119,7 +120,6 @@ require_svnserve () {
 }
 
 start_svnserve () {
-	SVNSERVE_PORT=${SVNSERVE_PORT-${this_test#t}}
 	svnserve --listen-port $SVNSERVE_PORT \
 		 --root "$rawsvnrepo" \
 		 --listen-once \
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index a8729f8232..e465116ef9 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -82,7 +82,7 @@ case $(uname) in
 esac
 
 LIB_HTTPD_PATH=${LIB_HTTPD_PATH-"$DEFAULT_HTTPD_PATH"}
-LIB_HTTPD_PORT=${LIB_HTTPD_PORT-${this_test#t}}
+test_set_port LIB_HTTPD_PORT
 
 TEST_PATH="$TEST_DIRECTORY"/lib-httpd
 HTTPD_ROOT_PATH="$PWD"/httpd
diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh
index ba3887f178..0aca8d7588 100755
--- a/t/t0410-partial-clone.sh
+++ b/t/t0410-partial-clone.sh
@@ -480,7 +480,6 @@ test_expect_success 'gc stops traversal when a missing but promised object is re
 	! grep "$TREE_HASH" out
 '
 
-LIB_HTTPD_PORT=12345  # default port, 410, cannot be used as non-root
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 32e722db2e..cd9e60632d 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -260,7 +260,7 @@ test_lazy_prereq GIT_DAEMON '
 # This test spawns a daemon, so run it only if the user would be OK with
 # testing with git-daemon.
 test_expect_success PIPE,JGIT,GIT_DAEMON 'indicate no refs in standards-compliant empty remote' '
-	JGIT_DAEMON_PORT=${JGIT_DAEMON_PORT-${this_test#t}} &&
+	test_set_port JGIT_DAEMON_PORT &&
 	JGIT_DAEMON_PID= &&
 	git init --bare empty.git &&
 	>empty.git/git-daemon-export-ok &&
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 6b3bbf99e4..d9a602cd0f 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1263,3 +1263,39 @@ test_oid () {
 	fi &&
 	eval "printf '%s' \"\${$var}\""
 }
+
+# Choose a port number based on the test script's number and store it in
+# the given variable name, unless that variable already contains a number.
+test_set_port () {
+	local var=$1 port
+
+	if test $# -ne 1 || test -z "$var"
+	then
+		BUG "test_set_port requires a variable name"
+	fi
+
+	eval port=\"\${$var}\"
+	case "$port" in
+	"")
+		# No port is set in the given env var, use the test
+		# number as port number instead.
+		# Remove not only the leading 't', but all leading zeros
+		# as well, so the arithmetic below won't (mis)interpret
+		# a test number like '0123' as an octal value.
+		port=${this_test#${this_test%%[1-9]*}}
+		if test "${port:-0}" -lt 1024
+		then
+			# root-only port, use a larger one instead.
+			port=$(($port + 10000))
+		fi
+
+		eval $var=$port
+		;;
+	*[^0-9]*)
+		error >&7 "invalid port number: $port"
+		;;
+	*)
+		# The user has specified the port.
+		;;
+	esac
+}
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Jeff King· Dec 5, 2018, 05:17 UTC · re: SZEDER Gábor · lore

Re: [PATCH 2/3] test-lib-functions: introduce the 'test_set_port' helper function

On Tue, Dec 04, 2018 at 05:34:56PM +0100, SZEDER Gábor wrote:
Show 11 quoted lines
> Several test scripts run daemons like 'git-daemon' or Apache, and
> communicate with them through TCP sockets.  To have unique ports where
> these daemons are accessible, the ports are usually the number of the
> corresponding test scripts, unless the user overrides them via
> environment variables, and thus all those tests and test libs contain
> more or less the same bit of one-liner boilerplate code to find out
> the port.  The last patch in this series will make this a bit more
> complicated.
> 
> Factor out finding the port for a daemon into the common helper
> function 'test_set_port' to avoid repeating ourselves.

OK. Looks like this should keep the same port numbers for all of the existing tests, which I think is good. As nice as choosing random high port numbers might be for some cases, it can also be confusing when there are random conflicts.

I've also run into non-random conflicts, but at least once you figure them out they're predictable (the most confusing I've seen is that adb, the android debugger, sometimes but not always leaves a daemon hanging around on port 5561).

Show 7 quoted lines
> Take special care of test scripts with "low" numbers:
> 
>   - Test numbers below 1024 would result in a port that's only usable
>     as root, so set their port to '10000 + test-nr' to make sure it
>     doesn't interfere with other tests in the test suite.  This makes
>     the hardcoded port number in 't0410-partial-clone.sh' unnecessary,
>     remove it.

This part is what made me think a bit more about conflicting with dynamically allocated ports. Arguably the http parts of t0410 ought to be in a much higher-numbered script, but I don't know that we've held over the years very well to the idea that scripts should only depend on the bits from lower numbered scripts.

Show 9 quoted lines
> ---
>  t/lib-git-daemon.sh      |  2 +-
>  t/lib-git-p4.sh          |  9 +--------
>  t/lib-git-svn.sh         |  2 +-
>  t/lib-httpd.sh           |  2 +-
>  t/t0410-partial-clone.sh |  1 -
>  t/t5512-ls-remote.sh     |  2 +-
>  t/test-lib-functions.sh  | 36 ++++++++++++++++++++++++++++++++++++
>  7 files changed, 41 insertions(+), 13 deletions(-)
The patch itself looks good to me.
> +	eval port=\"\${$var}\"
> +	case "$port" in

The quotes in the eval'd variable assignment aren't necessary. Usually I don't mind them in a simple:

  FOO="$bar"

even though they're redundant (and there were a few instances in the prior patch, I think). But here the escaping makes it harder to read, compared to:

  eval port=\$$var

It might be worth simplifying, but I don't feel strongly about it (we'd run into problems if $var contained spaces with either variant, but I don't think that's worth caring about).

-Peff
SZEDER Gábor· Dec 5, 2018, 12:20 UTC · re: Jeff King · lore

Re: [PATCH 2/3] test-lib-functions: introduce the 'test_set_port' helper function

On Wed, Dec 05, 2018 at 12:17:09AM -0500, Jeff King wrote:
Show 16 quoted lines
> On Tue, Dec 04, 2018 at 05:34:56PM +0100, SZEDER Gábor wrote:
> 
> > Several test scripts run daemons like 'git-daemon' or Apache, and
> > communicate with them through TCP sockets.  To have unique ports where
> > these daemons are accessible, the ports are usually the number of the
> > corresponding test scripts, unless the user overrides them via
> > environment variables, and thus all those tests and test libs contain
> > more or less the same bit of one-liner boilerplate code to find out
> > the port.  The last patch in this series will make this a bit more
> > complicated.
> > 
> > Factor out finding the port for a daemon into the common helper
> > function 'test_set_port' to avoid repeating ourselves.
> 
> OK. Looks like this should keep the same port numbers for all of the
> existing tests, which I think is good.

Not for all existing tests, though: t0410 and the 'git p4' tests do get different ports.

Hrm, speaking of affecting 'git p4' tests... I should have put Luke on Cc, so doing it now.

Jeff King· Dec 5, 2018, 21:59 UTC · re: SZEDER Gábor · lore

Re: [PATCH 2/3] test-lib-functions: introduce the 'test_set_port' helper function

On Wed, Dec 05, 2018 at 01:20:35PM +0100, SZEDER Gábor wrote:
Show 20 quoted lines
> On Wed, Dec 05, 2018 at 12:17:09AM -0500, Jeff King wrote:
> > On Tue, Dec 04, 2018 at 05:34:56PM +0100, SZEDER Gábor wrote:
> > 
> > > Several test scripts run daemons like 'git-daemon' or Apache, and
> > > communicate with them through TCP sockets.  To have unique ports where
> > > these daemons are accessible, the ports are usually the number of the
> > > corresponding test scripts, unless the user overrides them via
> > > environment variables, and thus all those tests and test libs contain
> > > more or less the same bit of one-liner boilerplate code to find out
> > > the port.  The last patch in this series will make this a bit more
> > > complicated.
> > > 
> > > Factor out finding the port for a daemon into the common helper
> > > function 'test_set_port' to avoid repeating ourselves.
> > 
> > OK. Looks like this should keep the same port numbers for all of the
> > existing tests, which I think is good.
> 
> Not for all existing tests, though: t0410 and the 'git p4' tests do
> get different ports.

Yeah, I should have said "most". The important thing is that they remain static and predictable for normal non-stress runs, which they do.

-Peff
SZEDER Gábor· Dec 4, 2018, 16:34 UTC · re: SZEDER Gábor · lore

[RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

Unfortunately, we have a few flaky tests, whose failures tend to be hard to reproduce. We've found that the best we can do to reproduce such a failure is to run the test repeatedly while the machine is under load, and wait in the hope that the load creates enough variance in the timing of the test's commands that a failure is evenually triggered. I have a command to do that, and I noticed that two other contributors have rolled their own scripts to do the same, all choosing slightly different approaches.

To help reproduce failures in flaky tests, introduce the '--stress' option to run a test script repeatedly in multiple parallel invocations until one of them fails, thereby using the test script itself to increase the load on the machine.

The number of parallel invocations is determined by, in order of precedence: the number specified as '--stress=<N>', or the value of the GIT_TEST_STRESS_LOAD environment variable, or twice the number of available processors in '/proc/cpuinfo', or 8.

To prevent the several parallel invocations of the same test from interfering with each other:

  - Include the parallel job's number in the name of the trash
    directory and the various output files under 't/test-results/' as
    a '.stress-<Nr>' suffix.
  - Add the parallel job's number to the port number specified by the
    user or to the test number, so even tests involving daemons
    listening on a TCP socket can be stressed.
  - Make '--stress' imply '--verbose-log' and discard the test's
    standard ouput and error; dumping the output of several parallel
    tests to the terminal would create a big ugly mess.

'wait' for all parallel jobs before exiting (either because a failure was found or because the user lost patience and aborted the stress test), allowing the still running tests to finish. Otherwise the "OK X.Y" progress output from the last iteration would likely arrive after the user got back the shell prompt, interfering with typing in the next command. OTOH, this waiting might induce a considerable delay between hitting ctrl-C and the test actually exiting; I'm not sure this is the right tradeoff.

Based on Jeff King's 'stress' script.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/README                | 13 ++++++-
 t/test-lib-functions.sh |  7 +++-
 t/test-lib.sh           | 82 +++++++++++++++++++++++++++++++++++++++--
 3 files changed, 96 insertions(+), 6 deletions(-)
Show changes to 3 files +96 −6

t/README, t/test-lib-functions.sh, t/test-lib.sh

diff --git a/t/README b/t/README
index 28711cc508..9851de25c2 100644
--- a/t/README
+++ b/t/README
@@ -186,6 +186,16 @@ appropriately before running "make".
 	this feature by setting the GIT_TEST_CHAIN_LINT environment
 	variable to "1" or "0", respectively.
 
+--stress::
+--stress=<N>::
+	Run the test script repeatedly in multiple parallel
+	invocations until one of them fails.  Useful for reproducing
+	rare failures in flaky tests.  The number of parallel
+	invocations is, in order of precedence: <N>, or the value of
+	the GIT_TEST_STRESS_LOAD environment variable, or twice the
+	number of available processors in '/proc/cpuinfo', or 8.
+	Implies `--verbose-log`.
+
 You can also set the GIT_TEST_INSTALLED environment variable to
 the bindir of an existing git installation to test that installation.
 You still need to have built this git sandbox, from which various
@@ -425,7 +435,8 @@ This test harness library does the following things:
  - Creates an empty test directory with an empty .git/objects database
    and chdir(2) into it.  This directory is 't/trash
    directory.$test_name_without_dotsh', with t/ subject to change by
-   the --root option documented above.
+   the --root option documented above, and a '.stress-<N>' suffix
+   appended by the --stress option.
 
  - Defines standard test helper functions for your scripts to
    use.  These functions are designed to make all scripts behave
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index d9a602cd0f..9af11e3eed 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1288,8 +1288,6 @@ test_set_port () {
 			# root-only port, use a larger one instead.
 			port=$(($port + 10000))
 		fi
-
-		eval $var=$port
 		;;
 	*[^0-9]*)
 		error >&7 "invalid port number: $port"
@@ -1298,4 +1296,9 @@ test_set_port () {
 		# The user has specified the port.
 		;;
 	esac
+
+	# Make sure that parallel '--stress' test jobs get different
+	# ports.
+	port=$(($port + ${GIT_TEST_STRESS_JOB_NR:-0}))
+	eval $var=$port
 }
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 49e4563405..9b7f687396 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -71,8 +71,81 @@ then
 	exit 1
 fi
 
+TEST_STRESS_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
 TEST_NAME="$(basename "$0" .sh)"
-TEST_RESULTS_BASE="$TEST_OUTPUT_DIRECTORY/test-results/$TEST_NAME"
+TEST_RESULTS_BASE="$TEST_OUTPUT_DIRECTORY/test-results/$TEST_NAME$TEST_STRESS_SFX"
+
+# If --stress was passed, run this test repeatedly in several parallel loops.
+case "$GIT_TEST_STRESS_STARTED, $* " in
+done,*)
+	# Don't stress test again.
+	;;
+*' --stress '*|*' '--stress=*' '*)
+	job_count=${*##*--stress=}
+	if test "$job_count" != "$*"
+	then
+		job_count=${job_count%% *}
+	elif test -n "$GIT_TEST_STRESS_LOAD"
+	then
+		job_count="$GIT_TEST_STRESS_LOAD"
+	elif test -r /proc/cpuinfo
+	then
+		job_count=$((2 * $(grep -c ^processor /proc/cpuinfo)))
+	else
+		job_count=8
+	fi
+
+	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
+	stressfail="$TEST_RESULTS_BASE.stress-failed"
+	rm -f "$stressfail"
+	trap 'echo aborted >"$stressfail"' TERM INT HUP
+
+	job_nr=0
+	while test $job_nr -lt "$job_count"
+	do
+		(
+			GIT_TEST_STRESS_STARTED=done
+			GIT_TEST_STRESS_JOB_NR=$job_nr
+			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
+
+			cnt=0
+			while ! test -e "$stressfail"
+			do
+				if $TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1
+				then
+					printf >&2 "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				elif test -f "$stressfail" &&
+				     test "$(cat "$stressfail")" = "aborted"
+				then
+					printf >&2 "ABORTED %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				else
+					printf >&2 "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
+				fi
+				cnt=$(($cnt + 1))
+			done
+		) &
+		job_nr=$(($job_nr + 1))
+	done
+
+	job_nr=0
+	while test $job_nr -lt "$job_count"
+	do
+		wait
+		job_nr=$(($job_nr + 1))
+	done
+
+	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
+	then
+		echo "Log(s) of failed test run(s) be found in:"
+		for f in $(cat "$stressfail")
+		do
+			echo "  $TEST_RESULTS_BASE.stress-$f.out"
+		done
+	fi
+	exit
+	;;
+esac
 
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
@@ -80,7 +153,7 @@ case "$GIT_TEST_TEE_STARTED, $* " in
 done,*)
 	# do not redirect again
 	;;
-*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
+*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*|*' --stress '*|*' '--stress=*' '*)
 	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
 
 	# Make this filename available to the sub-process in case it is using
@@ -341,6 +414,9 @@ do
 	-V|--verbose-log)
 		verbose_log=t
 		shift ;;
+	--stress|--stress=*)
+		verbose_log=t
+		shift ;;
 	*)
 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
 	esac
@@ -1028,7 +1104,7 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
+TRASH_DIRECTORY="trash directory.$TEST_NAME$TEST_STRESS_SFX"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Ævar Arnfjörð Bjarmason· Dec 4, 2018, 17:04 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04 2018, SZEDER Gábor wrote:
> The number of parallel invocations is determined by, in order of
> precedence: the number specified as '--stress=<N>', or the value of
> the GIT_TEST_STRESS_LOAD environment variable, or twice the number of
> available processors in '/proc/cpuinfo', or 8.

With this series we have at least 3 ways to get this number. First online_cpus() in the C code, then Peff's recent `getconf _NPROCESSORS_ONLN` in doc-diff, and now this /proc/cpuinfo parsing.

Perhaps it makes sense to split online_cpus() into a helper to use from the shellscripts instead?

SZEDER Gábor· Dec 4, 2018, 17:37 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04, 2018 at 06:04:14PM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 14 quoted lines
> 
> On Tue, Dec 04 2018, SZEDER Gábor wrote:
> 
> > The number of parallel invocations is determined by, in order of
> > precedence: the number specified as '--stress=<N>', or the value of
> > the GIT_TEST_STRESS_LOAD environment variable, or twice the number of
> > available processors in '/proc/cpuinfo', or 8.
> 
> With this series we have at least 3 ways to get this number. First
> online_cpus() in the C code, then Peff's recent `getconf
> _NPROCESSORS_ONLN` in doc-diff, and now this /proc/cpuinfo parsing.
> 
> Perhaps it makes sense to split online_cpus() into a helper to use from
> the shellscripts instead?

I don't think so, but I will update this patch to use 'getconf' instead.

Jeff King· Dec 5, 2018, 05:46 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04, 2018 at 06:04:14PM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 11 quoted lines
> 
> On Tue, Dec 04 2018, SZEDER Gábor wrote:
> 
> > The number of parallel invocations is determined by, in order of
> > precedence: the number specified as '--stress=<N>', or the value of
> > the GIT_TEST_STRESS_LOAD environment variable, or twice the number of
> > available processors in '/proc/cpuinfo', or 8.
> 
> With this series we have at least 3 ways to get this number. First
> online_cpus() in the C code, then Peff's recent `getconf
> _NPROCESSORS_ONLN` in doc-diff, and now this /proc/cpuinfo parsing.

To be fair, I only added the "getconf" thing because somebody complained that I was parsing /proc/cpuinfo, and suggested it instead. ;)

I don't think it's especially portable, but it should work on Linux and modern BSD/macOS, which may be enough (unlike doc-diff, I'd be a little more inclined to ask somebody on a random platform to stress test if they're hitting a bug).

> Perhaps it makes sense to split online_cpus() into a helper to use from
> the shellscripts instead?

I think somebody proposed that in a recent thread for other reasons, too. I'd be OK with that, but probably just using getconf is reasonable in the meantime.

-Peff
Ævar Arnfjörð Bjarmason· Dec 4, 2018, 18:11 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04 2018, SZEDER Gábor wrote:
Show 42 quoted lines
> Unfortunately, we have a few flaky tests, whose failures tend to be
> hard to reproduce.  We've found that the best we can do to reproduce
> such a failure is to run the test repeatedly while the machine is
> under load, and wait in the hope that the load creates enough variance
> in the timing of the test's commands that a failure is evenually
> triggered.  I have a command to do that, and I noticed that two other
> contributors have rolled their own scripts to do the same, all
> choosing slightly different approaches.
>
> To help reproduce failures in flaky tests, introduce the '--stress'
> option to run a test script repeatedly in multiple parallel
> invocations until one of them fails, thereby using the test script
> itself to increase the load on the machine.
>
> The number of parallel invocations is determined by, in order of
> precedence: the number specified as '--stress=<N>', or the value of
> the GIT_TEST_STRESS_LOAD environment variable, or twice the number of
> available processors in '/proc/cpuinfo', or 8.
>
> To prevent the several parallel invocations of the same test from
> interfering with each other:
>
>   - Include the parallel job's number in the name of the trash
>     directory and the various output files under 't/test-results/' as
>     a '.stress-<Nr>' suffix.
>
>   - Add the parallel job's number to the port number specified by the
>     user or to the test number, so even tests involving daemons
>     listening on a TCP socket can be stressed.
>
>   - Make '--stress' imply '--verbose-log' and discard the test's
>     standard ouput and error; dumping the output of several parallel
>     tests to the terminal would create a big ugly mess.
>
> 'wait' for all parallel jobs before exiting (either because a failure
> was found or because the user lost patience and aborted the stress
> test), allowing the still running tests to finish.  Otherwise the "OK
> X.Y" progress output from the last iteration would likely arrive after
> the user got back the shell prompt, interfering with typing in the
> next command.  OTOH, this waiting might induce a considerable delay
> between hitting ctrl-C and the test actually exiting; I'm not sure
> this is the right tradeoff.

I think it makes sense to generalize this and split it up into two features.

It's a frequent annoyance of mine in the test suite that I'm e.g. running t*.sh with some parallel "prove" in one screen, and then I run tABCD*.sh manually, and get unlucky because they use the same trash dir, and both tests go boom.

You can fix that with --root, which is much of what this patch does. My one-liner for doing --stress has been something like:

    perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh --root=trash-{} -v'

But it would be great if I didn't have to worry about that and could just run two concurrent:

    ./t0000-basic.sh

So I think we could just set some env variable where instead of having the predictable trash directory we have a $TRASHDIR.$N as this patch does, except we pick $N by locking some test-runs/tABCD.Nth file with flock() during the run.

Then a stress mode like this would just be:
    GIT_TEST_FLOCKED_TRASH_DIRS=1 perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh'

And sure, we could ship a --stress option too, but it wouldn't be magical in any way, just another "spawn N in a loop" implementation, but you could also e.g. use GNU parallel to drive it, and without needing to decide to stress test in advance, since we'd either flock() the trash dir, or just mktemp(1)-it.

Jeff King· Dec 5, 2018, 05:50 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04, 2018 at 07:11:08PM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 19 quoted lines
> It's a frequent annoyance of mine in the test suite that I'm
> e.g. running t*.sh with some parallel "prove" in one screen, and then I
> run tABCD*.sh manually, and get unlucky because they use the same trash
> dir, and both tests go boom.
> 
> You can fix that with --root, which is much of what this patch does. My
> one-liner for doing --stress has been something like:
> 
>     perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh --root=trash-{} -v'
> 
> But it would be great if I didn't have to worry about that and could
> just run two concurrent:
> 
>     ./t0000-basic.sh
> 
> So I think we could just set some env variable where instead of having
> the predictable trash directory we have a $TRASHDIR.$N as this patch
> does, except we pick $N by locking some test-runs/tABCD.Nth file with
> flock() during the run.

That actually sounds kind of annoying when doing a single run, since now you have this extra ".N". I guess it would at least be predictable, and I tend to tab-complete the trash dirs anyway.

I accomplish the same thing by doing my "big" runs using --root specified in config.mak (which points to a RAM disk -- if you're not using one to run the tests, you really should look into it, as it's way faster). And then for one-offs, investigating failures, etc, I do "cd t && ./t0000-basic.sh -v -i", which naturally runs in the local directory.

-Peff
SZEDER Gábor· Dec 5, 2018, 12:07 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04, 2018 at 07:11:08PM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 19 quoted lines
> It's a frequent annoyance of mine in the test suite that I'm
> e.g. running t*.sh with some parallel "prove" in one screen, and then I
> run tABCD*.sh manually, and get unlucky because they use the same trash
> dir, and both tests go boom.
> 
> You can fix that with --root, which is much of what this patch does. My
> one-liner for doing --stress has been something like:
> 
>     perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh --root=trash-{} -v'
> 
> But it would be great if I didn't have to worry about that and could
> just run two concurrent:
> 
>     ./t0000-basic.sh
> 
> So I think we could just set some env variable where instead of having
> the predictable trash directory we have a $TRASHDIR.$N as this patch
> does, except we pick $N by locking some test-runs/tABCD.Nth file with
> flock() during the run.
Is 'flock' portable?  It doesn't appear so.
> Then a stress mode like this would just be:
> 
>     GIT_TEST_FLOCKED_TRASH_DIRS=1 perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh'
This doesn't address the issue of TCP ports for various daemons.
> And sure, we could ship a --stress option too, but it wouldn't be
> magical in any way, just another "spawn N in a loop" implementation, but
> you could also e.g. use GNU parallel to drive it, and without needing to

GNU 'parallel' may or may not be available on the platform, that's why I stuck with the barebones shell-loops-in-the-background approach.

> decide to stress test in advance, since we'd either flock() the trash
> dir, or just mktemp(1)-it.

While 'mktemp' seems to be more portable than 'flock', it doesn't seem to be portable enough; at least it's not in POSIX.

And in general I'd prefer deterministic trash directory names. If I re-run a failed test after fixing the bug, then currently the trash directory will be overwritten, and that's good. With 'mktemp's junk in the trash direcory's name it won't be overwritten, and if my bugfix doesn't work, then I'll start accumulating trash directories and won't even know which one is from the last run.

Ævar Arnfjörð Bjarmason· Dec 5, 2018, 14:01 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05 2018, SZEDER Gábor wrote:
Show 22 quoted lines
> On Tue, Dec 04, 2018 at 07:11:08PM +0100, Ævar Arnfjörð Bjarmason wrote:
>> It's a frequent annoyance of mine in the test suite that I'm
>> e.g. running t*.sh with some parallel "prove" in one screen, and then I
>> run tABCD*.sh manually, and get unlucky because they use the same trash
>> dir, and both tests go boom.
>>
>> You can fix that with --root, which is much of what this patch does. My
>> one-liner for doing --stress has been something like:
>>
>>     perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh --root=trash-{} -v'
>>
>> But it would be great if I didn't have to worry about that and could
>> just run two concurrent:
>>
>>     ./t0000-basic.sh
>>
>> So I think we could just set some env variable where instead of having
>> the predictable trash directory we have a $TRASHDIR.$N as this patch
>> does, except we pick $N by locking some test-runs/tABCD.Nth file with
>> flock() during the run.
>
> Is 'flock' portable?  It doesn't appear so.

Portable enough, and since it's just an alias for "grab lock atomically" it can devolve into other more basic FS functions on systems that don't have it.

Show 5 quoted lines
>> Then a stress mode like this would just be:
>>
>>     GIT_TEST_FLOCKED_TRASH_DIRS=1 perl -E 'say ++$_ while 1' | parallel --jobs=100% --halt-on-error soon,fail=1 './t0000-basic.sh'
>
> This doesn't address the issue of TCP ports for various daemons.

Once we have a test ABCD and slot offset N (for a fixed size of N) we can pick a port from that.

Show 6 quoted lines
>> And sure, we could ship a --stress option too, but it wouldn't be
>> magical in any way, just another "spawn N in a loop" implementation, but
>> you could also e.g. use GNU parallel to drive it, and without needing to
>
> GNU 'parallel' may or may not be available on the platform, that's why
> I stuck with the barebones shell-loops-in-the-background approach.

Yes, my point is not that you should drop that part of your patch for using GNU parallel, but just to demonstrate that we can get pretty close to this for most tests with an external tool, and that it would make this sort of thing work for concurrent tests without needing to decide to concurrently test in advance.

Show 5 quoted lines
>> decide to stress test in advance, since we'd either flock() the trash
>> dir, or just mktemp(1)-it.
>
> While 'mktemp' seems to be more portable than 'flock', it doesn't seem
> to be portable enough; at least it's not in POSIX.>

We are already relying on stuff like mktemp() being reliable for e.g. the split index.

Show 6 quoted lines
> And in general I'd prefer deterministic trash directory names.  If I
> re-run a failed test after fixing the bug, then currently the trash
> directory will be overwritten, and that's good.  With 'mktemp's junk
> in the trash direcory's name it won't be overwritten, and if my bugfix
> doesn't work, then I'll start accumulating trash directories and won't
> even know which one is from the last run.

In general you wouldn't need to set GIT_TEST_FLOCKED_TRASH_DIRS=1 and would get the same monolithic trash names as now, and for something like the flock() version it could use a job number as a suffix like your patch does.

SZEDER Gábor· Dec 5, 2018, 14:39 UTC · re: Ævar Arnfjörð Bjarmason · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 03:01:41PM +0100, Ævar Arnfjörð Bjarmason wrote:
Show 8 quoted lines
> >> decide to stress test in advance, since we'd either flock() the trash
> >> dir, or just mktemp(1)-it.
> >
> > While 'mktemp' seems to be more portable than 'flock', it doesn't seem
> > to be portable enough; at least it's not in POSIX.>
> 
> We are already relying on stuff like mktemp() being reliable for
> e.g. the split index.

That's the mktemp() function from libc; I meant the 'mktemp' command that we would use this early in 'test-lib.sh', where PATH has not been set up for testing yet.

Ævar Arnfjörð Bjarmason· Dec 5, 2018, 19:59 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05 2018, SZEDER Gábor wrote:
Show 13 quoted lines
> On Wed, Dec 05, 2018 at 03:01:41PM +0100, Ævar Arnfjörð Bjarmason wrote:
>> >> decide to stress test in advance, since we'd either flock() the trash
>> >> dir, or just mktemp(1)-it.
>> >
>> > While 'mktemp' seems to be more portable than 'flock', it doesn't seem
>> > to be portable enough; at least it's not in POSIX.>
>>
>> We are already relying on stuff like mktemp() being reliable for
>> e.g. the split index.
>
> That's the mktemp() function from libc; I meant the 'mktemp' command
> that we would use this early in 'test-lib.sh', where PATH has not been
> set up for testing yet.
Ah, if that ever becomes an issue it's a one-line test-tool wrapper.
Jeff King· Dec 5, 2018, 05:44 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Tue, Dec 04, 2018 at 05:34:57PM +0100, SZEDER Gábor wrote:
Show 6 quoted lines
> To prevent the several parallel invocations of the same test from
> interfering with each other:
> 
>   - Include the parallel job's number in the name of the trash
>     directory and the various output files under 't/test-results/' as
>     a '.stress-<Nr>' suffix.
Makes sense.
>   - Add the parallel job's number to the port number specified by the
>     user or to the test number, so even tests involving daemons
>     listening on a TCP socket can be stressed.

In my script I just use an arbitrary sequence of ports. That means two stress runs may stomp on each other, but stress runs tend not to interfere with regular runs (whereas with your scheme, a stress run of t5561 is going to stomp on t5562). It probably doesn't matter much either way, as I tend not to do too much with the machine during a stress run.

>   - Make '--stress' imply '--verbose-log' and discard the test's
>     standard ouput and error; dumping the output of several parallel
>     tests to the terminal would create a big ugly mess.

Makes sense. My script just redirects the output to a file, but it predates --verbose-log.

My script always runs with "-x". I guess it's not that hard to add it if you want, but it is annoying to finally get a failure after several minutes only to realize that your are missing some important information. ;)

Ditto for "-i", which leaves more readable output (you know the interesting part is at the end of the log), and leaves a trash directory state that is more likely to be useful for inspecting.

My script also implies "--root". That's not strictly necessary, though I suspect it is much more likely to catch races when it's run on a ram disk (simply because it leaves the CPU as the main bottleneck).

Show 8 quoted lines
> 'wait' for all parallel jobs before exiting (either because a failure
> was found or because the user lost patience and aborted the stress
> test), allowing the still running tests to finish.  Otherwise the "OK
> X.Y" progress output from the last iteration would likely arrive after
> the user got back the shell prompt, interfering with typing in the
> next command.  OTOH, this waiting might induce a considerable delay
> between hitting ctrl-C and the test actually exiting; I'm not sure
> this is the right tradeoff.

If there is a delay, it's actually quite annoying to _not_ wait; you start doing something in the terminal, and then a bunch of extra test statuses print at a random time.

Show 14 quoted lines
> +	job_nr=0
> +	while test $job_nr -lt "$job_count"
> +	do
> +		(
> +			GIT_TEST_STRESS_STARTED=done
> +			GIT_TEST_STRESS_JOB_NR=$job_nr
> +			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
> +
> +			cnt=0
> +			while ! test -e "$stressfail"
> +			do
> +				if $TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1
> +				then
> +					printf >&2 "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt

OK, this adds a counter for each job number (compared to my script). Seems helpful.

Show 8 quoted lines
> +				elif test -f "$stressfail" &&
> +				     test "$(cat "$stressfail")" = "aborted"
> +				then
> +					printf >&2 "ABORTED %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> +				else
> +					printf >&2 "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> +					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
> +				fi

Hmm. What happens if we see a failure _and_ there's an abort? That's actually pretty plausible if you see a failure whiz by, and you want to abort the remaining scripts because they take a long time to run.

If the abort happens first, then the goal is to assume any errors are due to the abort. But there's a race between the individual jobs reading $stressfail and the parent signal handler writing it. So you may end up with "aborted\n5" or similar (after which any other jobs would fail to match "aborted" and declare themselves failures, as well). I think my script probably also has this race, too (it doesn't check the abort case explicitly, but it would race in checking "test -e fail").

If the fail happens first (which I think is the more likely case), then the abort handler will overwrite the file with "aborted", and we'll forget that there was a real failure. This works in my script because of the symlinking (if a real failure symlinked $fail to a directory, then the attempt to write "aborted" will just be a noop).

Show 6 quoted lines
> +	job_nr=0
> +	while test $job_nr -lt "$job_count"
> +	do
> +		wait
> +		job_nr=$(($job_nr + 1))
> +	done

Do we need to loop? Calling "wait" with no arguments should wait for all children.

Show 8 quoted lines
> +	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
> +	then
> +		echo "Log(s) of failed test run(s) be found in:"
> +		for f in $(cat "$stressfail")
> +		do
> +			echo "  $TEST_RESULTS_BASE.stress-$f.out"
> +		done
> +	fi

In my experience, outputting the failed log saves a step (especially with "-i"), since seeing the failed test and its output is often sufficient.

I'm also sad to lose the symlink to the failed trash directory, which saves cutting and pasting when you have to inspect it. But I guess we can't rely on symlinks everywhere.

-Peff
SZEDER Gábor· Dec 5, 2018, 10:34 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

Just a quick reply to this one point for now:
On Wed, Dec 05, 2018 at 12:44:09AM -0500, Jeff King wrote:
Show 10 quoted lines
> On Tue, Dec 04, 2018 at 05:34:57PM +0100, SZEDER Gábor wrote:
> > +	job_nr=0
> > +	while test $job_nr -lt "$job_count"
> > +	do
> > +		wait
> > +		job_nr=$(($job_nr + 1))
> > +	done
> 
> Do we need to loop? Calling "wait" with no arguments should wait for all
> children.

It should, but in dash, ksh, ksh93, Bash v4.2 or older it doesn't seem to do so, at least not on my machine, and I get back my shell prompt right after hitting ctrl-C or the first failed test, and then get the progress output from the remaining jobs later.

Bash 4.3 or later are strange: I get back the shell prompt immediately after ctrl-C as well, so it doesn't appear to be waiting for all remaining jobs to finish either, but! I don't get any of the progress output from those jobs to mess up my next command.

And mksh and zsh can't run our tests, and I don't have any more shells at hand to try.

Jeff King· Dec 5, 2018, 21:36 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 11:34:54AM +0100, SZEDER Gábor wrote:
Show 19 quoted lines
> 
> Just a quick reply to this one point for now:
> 
> On Wed, Dec 05, 2018 at 12:44:09AM -0500, Jeff King wrote:
> > On Tue, Dec 04, 2018 at 05:34:57PM +0100, SZEDER Gábor wrote:
> > > +	job_nr=0
> > > +	while test $job_nr -lt "$job_count"
> > > +	do
> > > +		wait
> > > +		job_nr=$(($job_nr + 1))
> > > +	done
> > 
> > Do we need to loop? Calling "wait" with no arguments should wait for all
> > children.
> 
> It should, but in dash, ksh, ksh93, Bash v4.2 or older it doesn't seem
> to do so, at least not on my machine, and I get back my shell prompt
> right after hitting ctrl-C or the first failed test, and then get the
> progress output from the remaining jobs later.

That's weird. I run my stress script under dash with a single "wait", and it handles the failing case just fine. And switching to a single "wait" in your script works for me.

But the ^C case is interesting. Try running your script under "sh -x" and hitting ^C. The signal interrupts the first wait. In my script (or yours modified to use a single wait), we then proceed immediately to the "exit", and get output from the subshells after we've exited.

But in your script with the loop, the first wait is interrupted, and then the _second_ wait (in the second loop iteration) picks up all of the exiting processes. The subsequent waits are all noops that return immediately, because there are no processes left.

So the right number of waits is either "1" or "2". Looping means we do too many (which is mostly a harmless noop) or too few (in the off chance that you have only a single job ;) ). So it works out in practice.

But I think a more clear way to do it is to simply wait in the signal trap, to cover the case when our original wait was aborted.

I.e., something like this:
Show changes to 2 files +9 −7

t/test-lib.sh, t/t0000-basic.sh

diff --git a/t/test-lib.sh b/t/test-lib.sh
index 9b7f687396..5e0c41626f 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -98,7 +98,7 @@ done,*)
 	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
 	stressfail="$TEST_RESULTS_BASE.stress-failed"
 	rm -f "$stressfail"
-	trap 'echo aborted >"$stressfail"' TERM INT HUP
+	trap 'echo aborted >"$stressfail"; wait' TERM INT HUP
 
 	job_nr=0
 	while test $job_nr -lt "$job_count"
@@ -128,12 +128,7 @@ done,*)
 		job_nr=$(($job_nr + 1))
 	done
 
-	job_nr=0
-	while test $job_nr -lt "$job_count"
-	do
-		wait
-		job_nr=$(($job_nr + 1))
-	done
+	wait
 
 	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
 	then

which behaves well for me in all cases.

> Bash 4.3 or later are strange: I get back the shell prompt immediately
> after ctrl-C as well, so it doesn't appear to be waiting for all
> remaining jobs to finish either, but! I don't get any of the progress
> output from those jobs to mess up my next command.

Interesting. My bash 4.4 seems to behave the same as dash. It almost
sounds like the SIGINT is getting passed to the subshells for you.
Probably not really worth digging into, though.

In case anybody is playing with this and needed to simulate a stress
failure, here's what I used:

diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index b6566003dd..a370cd9977 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -1171,4 +1171,11 @@ test_expect_success 'very long name in the index handled sanely' '
 	test $len = 4098
 '
 
+test_expect_success 'roll those dice' '
+	case "$(openssl rand -base64 1)" in
+	z*)
+		return 1
+	esac
+'
+
 test_done
Junio C Hamano· Dec 6, 2018, 00:22 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

Jeff King <peff@peff.net> writes:
Show 13 quoted lines
> But the ^C case is interesting. Try running your script under "sh -x"
> and hitting ^C. The signal interrupts the first wait. In my script (or
> yours modified to use a single wait), we then proceed immediately to the
> "exit", and get output from the subshells after we've exited.
>
> But in your script with the loop, the first wait is interrupted, and
> then the _second_ wait (in the second loop iteration) picks up all of
> the exiting processes. The subsequent waits are all noops that return
> immediately, because there are no processes left.
>
> So the right number of waits is either "1" or "2". Looping means we do
> too many (which is mostly a harmless noop) or too few (in the off chance
> that you have only a single job ;) ). So it works out in practice.

Well, if you time your ^C perfectly, no number of waits is right, I am afraid. You spawn N processes and while looping N times waiting for them, you can ^C out of wait before these N processes all die, no?

> But I think a more clear way to do it is to simply wait in the signal
> trap, to cover the case when our original wait was aborted.
Sounds good.
Jeff King· Dec 6, 2018, 05:35 UTC · re: Junio C Hamano · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Thu, Dec 06, 2018 at 09:22:23AM +0900, Junio C Hamano wrote:
Show 8 quoted lines
> > So the right number of waits is either "1" or "2". Looping means we do
> > too many (which is mostly a harmless noop) or too few (in the off chance
> > that you have only a single job ;) ). So it works out in practice.
> 
> Well, if you time your ^C perfectly, no number of waits is right, I
> am afraid.  You spawn N processes and while looping N times waiting
> for them, you can ^C out of wait before these N processes all die,
> no?

Each "wait" will try to collect all processes, but may be interrupted by a signal. So the correct number is actually "1 plus the number of times the user hits ^C". I had assumed the user was just hitting it once, though putting the wait into the trap means we do that "1 plus" thing anyway.

I could also see an argument that subsequent ^C's should exit immediately, but I think we are getting well into the realm of over-engineering.

-Peff
Junio C Hamano· Dec 6, 2018, 06:41 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

Jeff King <peff@peff.net> writes:
> Each "wait" will try to collect all processes, but may be interrupted by
> a signal. So the correct number is actually "1 plus the number of times
> the user hits ^C".

Yeah and that is not bounded. It is OK not to catch multiple ^C that races with what we do, so having ane extra wait in the clean-up procedure after receiving a signal like you suggested would be both good enough and the cleanest solution, I think.

Thanks.
SZEDER Gábor· Dec 6, 2018, 22:56 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 04:36:26PM -0500, Jeff King wrote:
> The signal interrupts the first wait.

Ah, of course. I'm ashamed to say that this is not the first time I forget about that...

Show 8 quoted lines
> > Bash 4.3 or later are strange: I get back the shell prompt immediately
> > after ctrl-C as well, so it doesn't appear to be waiting for all
> > remaining jobs to finish either, but! I don't get any of the progress
> > output from those jobs to mess up my next command.
> 
> Interesting. My bash 4.4 seems to behave the same as dash. It almost
> sounds like the SIGINT is getting passed to the subshells for you.
> Probably not really worth digging into, though.

The subshell does indeed get SIGINT. I don't know why that happens, to my understanding that should not happen.

Show 17 quoted lines
> In case anybody is playing with this and needed to simulate a stress
> failure, here's what I used:
> 
> diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
> index b6566003dd..a370cd9977 100755
> --- a/t/t0000-basic.sh
> +++ b/t/t0000-basic.sh
> @@ -1171,4 +1171,11 @@ test_expect_success 'very long name in the index handled sanely' '
>  	test $len = 4098
>  '
>  
> +test_expect_success 'roll those dice' '
> +	case "$(openssl rand -base64 1)" in
> +	z*)
> +		return 1
> +	esac
> +'
Wasteful :)
  test $(($$ % 42)) -ne 0
Jeff King· Dec 7, 2018, 01:03 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Thu, Dec 06, 2018 at 11:56:01PM +0100, SZEDER Gábor wrote:
Show 10 quoted lines
> > +test_expect_success 'roll those dice' '
> > +	case "$(openssl rand -base64 1)" in
> > +	z*)
> > +		return 1
> > +	esac
> > +'
> 
> Wasteful :)
> 
>   test $(($$ % 42)) -ne 0
Oh, indeed, that is much more clever. :)
-Peff
SZEDER Gábor· Dec 5, 2018, 14:01 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 12:44:09AM -0500, Jeff King wrote:
Show 21 quoted lines
> On Tue, Dec 04, 2018 at 05:34:57PM +0100, SZEDER Gábor wrote:
> 
> > To prevent the several parallel invocations of the same test from
> > interfering with each other:
> > 
> >   - Include the parallel job's number in the name of the trash
> >     directory and the various output files under 't/test-results/' as
> >     a '.stress-<Nr>' suffix.
> 
> Makes sense.
> 
> >   - Add the parallel job's number to the port number specified by the
> >     user or to the test number, so even tests involving daemons
> >     listening on a TCP socket can be stressed.
> 
> In my script I just use an arbitrary sequence of ports. That means two
> stress runs may stomp on each other, but stress runs tend not to
> interfere with regular runs (whereas with your scheme, a stress run of
> t5561 is going to stomp on t5562). It probably doesn't matter much
> either way, as I tend not to do too much with the machine during a
> stress run.

A custom port can always be set via environment variables, though the user has to remember to set it (I doubt that I would remember it until reminded by test failures...)

Show 15 quoted lines
> >   - Make '--stress' imply '--verbose-log' and discard the test's
> >     standard ouput and error; dumping the output of several parallel
> >     tests to the terminal would create a big ugly mess.
> 
> Makes sense. My script just redirects the output to a file, but it
> predates --verbose-log.
> 
> My script always runs with "-x". I guess it's not that hard to add it if
> you want, but it is annoying to finally get a failure after several
> minutes only to realize that your are missing some important
> information. ;)
> 
> Ditto for "-i", which leaves more readable output (you know the
> interesting part is at the end of the log), and leaves a trash directory
> state that is more likely to be useful for inspecting.

I wanted to imply only the bare minimum of options that are necessary to prevent the tests from stomping on each other. Other than that I agree and wouldn't run '--stress' without '-x -i' myself, and at one point considered to recommend '-x -i' in the description of '--stress'.

Show 16 quoted lines
> My script also implies "--root". That's not strictly necessary, though I
> suspect it is much more likely to catch races when it's run on a ram
> disk (simply because it leaves the CPU as the main bottleneck).
> 
> > 'wait' for all parallel jobs before exiting (either because a failure
> > was found or because the user lost patience and aborted the stress
> > test), allowing the still running tests to finish.  Otherwise the "OK
> > X.Y" progress output from the last iteration would likely arrive after
> > the user got back the shell prompt, interfering with typing in the
> > next command.  OTOH, this waiting might induce a considerable delay
> > between hitting ctrl-C and the test actually exiting; I'm not sure
> > this is the right tradeoff.
> 
> If there is a delay, it's actually quite annoying to _not_ wait; you
> start doing something in the terminal, and then a bunch of extra test
> statuses print at a random time.

At least the INT/TERM/etc. handler should say something like "Waiting for background jobs to finish", to let the user know why it doesn't exit right away.

An alternative would be to exit right away, and make the background loops a tad bit smarter to not print these progress lines when aborting.

Show 33 quoted lines
> > +	job_nr=0
> > +	while test $job_nr -lt "$job_count"
> > +	do
> > +		(
> > +			GIT_TEST_STRESS_STARTED=done
> > +			GIT_TEST_STRESS_JOB_NR=$job_nr
> > +			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
> > +
> > +			cnt=0
> > +			while ! test -e "$stressfail"
> > +			do
> > +				if $TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1
> > +				then
> > +					printf >&2 "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> 
> OK, this adds a counter for each job number (compared to my script).
> Seems helpful.
> 
> > +				elif test -f "$stressfail" &&
> > +				     test "$(cat "$stressfail")" = "aborted"
> > +				then
> > +					printf >&2 "ABORTED %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> > +				else
> > +					printf >&2 "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> > +					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
> > +				fi
> 
> Hmm. What happens if we see a failure _and_ there's an abort? That's
> actually pretty plausible if you see a failure whiz by, and you want to
> abort the remaining scripts because they take a long time to run.
> 
> If the abort happens first, then the goal is to assume any errors are
> due to the abort.

Yeah, that's why I added this ABORTED case. When I hit ctrl-C, a couple of the tests tend to fail, but that's not the rare failure we are hoping to find when stress testing, so printing FAIL would be misleading. The test script exits with 1 all the same, so we can't tell the difference, but since rare failures are, well, rare, this failure is most likely due to the abort.

Show 12 quoted lines
> But there's a race between the individual jobs reading
> $stressfail and the parent signal handler writing it. So you may end up
> with "aborted\n5" or similar (after which any other jobs would fail to
> match "aborted" and declare themselves failures, as well).  I think my
> script probably also has this race, too (it doesn't check the abort case
> explicitly, but it would race in checking "test -e fail").
>
> If the fail happens first (which I think is the more likely case), then
> the abort handler will overwrite the file with "aborted", and we'll
> forget that there was a real failure. This works in my script because of
> the symlinking (if a real failure symlinked $fail to a directory, then
> the attempt to write "aborted" will just be a noop).

OK, I think the abort handler should append, not overwrite, and then the file's contents should be checked e.g. with a case statement looking for *aborted*.

Or use two separate files for signaling abort and test failure.

In any case, you could always grab the job number from the "FAIL X.Y" line, and then look at its logs and trash direcory.

Show 22 quoted lines
> > +	job_nr=0
> > +	while test $job_nr -lt "$job_count"
> > +	do
> > +		wait
> > +		job_nr=$(($job_nr + 1))
> > +	done
> 
> Do we need to loop? Calling "wait" with no arguments should wait for all
> children.
> 
> > +	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
> > +	then
> > +		echo "Log(s) of failed test run(s) be found in:"
> > +		for f in $(cat "$stressfail")
> > +		do
> > +			echo "  $TEST_RESULTS_BASE.stress-$f.out"
> > +		done
> > +	fi
> 
> In my experience, outputting the failed log saves a step (especially
> with "-i"), since seeing the failed test and its output is often
> sufficient.

I just don't like when test output, especially with '-x', is dumped on my terminal :)

> I'm also sad to lose the symlink to the failed trash directory, which
> saves cutting and pasting when you have to inspect it. But I guess we
> can't rely on symlinks everywhere.

You can tab-complete most of the trash directory, and then there are only those 1-2-3 digits of the job number that you have to type. That worked out well enough for me so far (but I've only used it for a few days, so...).

We could rename the trash directory of the failed test to something short, sweet, and common, but that could be racy in the unlikely case that two tests fail at the same time. And I like the 'trash directory.' prefix, because then 'make clean' will delete that one, too, without modifying 't/Makefile' (which is also the reason for adding the 'stress-N' suffix instead of a prefix, and putting the fail file into the 'test-results' directory).

Jeff King· Dec 5, 2018, 21:56 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 03:01:06PM +0100, SZEDER Gábor wrote:
Show 21 quoted lines
> > >   - Make '--stress' imply '--verbose-log' and discard the test's
> > >     standard ouput and error; dumping the output of several parallel
> > >     tests to the terminal would create a big ugly mess.
> > 
> > Makes sense. My script just redirects the output to a file, but it
> > predates --verbose-log.
> > 
> > My script always runs with "-x". I guess it's not that hard to add it if
> > you want, but it is annoying to finally get a failure after several
> > minutes only to realize that your are missing some important
> > information. ;)
> > 
> > Ditto for "-i", which leaves more readable output (you know the
> > interesting part is at the end of the log), and leaves a trash directory
> > state that is more likely to be useful for inspecting.
> 
> I wanted to imply only the bare minimum of options that are necessary
> to prevent the tests from stomping on each other.  Other than that I
> agree and wouldn't run '--stress' without '-x -i' myself, and at one
> point considered to recommend '-x -i' in the description of
> '--stress'.

Yeah, it probably is the right thing to make the options as orthogonal as possible, and let the user decide what they want. I was just wondering if I could replace my "stress" script with this. But I think I'd still want it to remember to use a sane set of options (including "--root" in my case).

Show 16 quoted lines
> > > 'wait' for all parallel jobs before exiting (either because a failure
> > > was found or because the user lost patience and aborted the stress
> > > test), allowing the still running tests to finish.  Otherwise the "OK
> > > X.Y" progress output from the last iteration would likely arrive after
> > > the user got back the shell prompt, interfering with typing in the
> > > next command.  OTOH, this waiting might induce a considerable delay
> > > between hitting ctrl-C and the test actually exiting; I'm not sure
> > > this is the right tradeoff.
> > 
> > If there is a delay, it's actually quite annoying to _not_ wait; you
> > start doing something in the terminal, and then a bunch of extra test
> > statuses print at a random time.
> 
> At least the INT/TERM/etc. handler should say something like "Waiting
> for background jobs to finish", to let the user know why it doesn't
> exit right away.

That seems reasonable. There's also no point in waiting for them to finish if we know we're aborting anyway. Could we just kill them all?

I guess it's a little tricky, because $! is going to give us the pid of each subshell. We actually want to kill the test sub-process. That takes a few contortions, but the output is nice (every sub-job immediately says "ABORTED ...", and the final process does not exit until the whole tree is done):

Show changes to t/test-lib.sh +8 −10
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 9b7f687396..357dead3ff 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -98,8 +98,9 @@ done,*)
 	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
 	stressfail="$TEST_RESULTS_BASE.stress-failed"
 	rm -f "$stressfail"
-	trap 'echo aborted >"$stressfail"' TERM INT HUP
+	trap 'echo aborted >"$stressfail"; kill $job_pids 2>/dev/null; wait' TERM INT HUP
 
+	job_pids=
 	job_nr=0
 	while test $job_nr -lt "$job_count"
 	do
@@ -108,10 +109,13 @@ done,*)
 			GIT_TEST_STRESS_JOB_NR=$job_nr
 			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
 
+			trap 'kill $test_pid 2>/dev/null' TERM
+
 			cnt=0
 			while ! test -e "$stressfail"
 			do
-				if $TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1
+				$TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1 & test_pid=$!
+				if wait
 				then
 					printf >&2 "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
 				elif test -f "$stressfail" &&
@@ -124,16 +128,11 @@ done,*)
 				fi
 				cnt=$(($cnt + 1))
 			done
-		) &
+		) & job_pids="$job_pids $!"
 		job_nr=$(($job_nr + 1))
 	done
 
-	job_nr=0
-	while test $job_nr -lt "$job_count"
-	do
-		wait
-		job_nr=$(($job_nr + 1))
-	done
+	wait
 
 	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
 	then

> > > +				elif test -f "$stressfail" &&
> > > +				     test "$(cat "$stressfail")" = "aborted"
> > > +				then
> > > +					printf >&2 "ABORTED %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> > > +				else
> > > +					printf >&2 "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
> > > +					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
> > > +				fi
> > 
> > Hmm. What happens if we see a failure _and_ there's an abort? That's
> > actually pretty plausible if you see a failure whiz by, and you want to
> > abort the remaining scripts because they take a long time to run.
> > 
> > If the abort happens first, then the goal is to assume any errors are
> > due to the abort.
> 
> Yeah, that's why I added this ABORTED case.  When I hit ctrl-C, a
> couple of the tests tend to fail, but that's not the rare failure we
> are hoping to find when stress testing, so printing FAIL would be
> misleading.  The test script exits with 1 all the same, so we can't
> tell the difference, but since rare failures are, well, rare, this
> failure is most likely due to the abort.

With the patch as posted I actually see most of them still say "OK",
because the SIGINT does not make it to them (it would be a different
story if I killed the whole process group).

But with the modification above, I get "ABORTED" for most, which is what
I'd want. And it should be pretty race-free, since the "aborted" file is
put into place before triggering the signal cascade.

> > If the fail happens first (which I think is the more likely case), then
> > the abort handler will overwrite the file with "aborted", and we'll
> > forget that there was a real failure. This works in my script because of
> > the symlinking (if a real failure symlinked $fail to a directory, then
> > the attempt to write "aborted" will just be a noop).
> 
> OK, I think the abort handler should append, not overwrite, and then
> the file's contents should be checked e.g. with a case statement
> looking for *aborted*.
> 
> Or use two separate files for signaling abort and test failure.

Two separate files seems a lot simpler to me.

> > In my experience, outputting the failed log saves a step (especially
> > with "-i"), since seeing the failed test and its output is often
> > sufficient.
> 
> I just don't like when test output, especially with '-x', is dumped on
> my terminal :)

Don't you then use your terminal to display the file? ;) (I'm kidding, I
assume you load it in "less" or whatever, which is distinct).

I wish this could be optional somehow, though. I really prefer the dump.
I guess if I continue to use my script as a wrapper, it can dump for me
(after digging in the $stressfail file, I guess).

> > I'm also sad to lose the symlink to the failed trash directory, which
> > saves cutting and pasting when you have to inspect it. But I guess we
> > can't rely on symlinks everywhere.
> 
> You can tab-complete most of the trash directory, and then there are
> only those 1-2-3 digits of the job number that you have to type.  That
> worked out well enough for me so far (but I've only used it for a few
> days, so...).
> 
> We could rename the trash directory of the failed test to something
> short, sweet, and common, but that could be racy in the unlikely case
> that two tests fail at the same time.  And I like the 'trash
> directory.' prefix, because then 'make clean' will delete that one,
> too, without modifying 't/Makefile' (which is also the reason for
> adding the 'stress-N' suffix instead of a prefix, and putting the fail
> file into the 'test-results' directory).

We could rename it to "trash directory.xxx.failed", which is at least
predictable. That can even be done atomically, though I think it's a
little tricky to do portably with shell tools. I guess I can continue to
do the symlink thing in my wrapper script.

-Peff
SZEDER Gábor· Dec 6, 2018, 23:10 UTC · re: Jeff King · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Wed, Dec 05, 2018 at 04:56:21PM -0500, Jeff King wrote:
Show 7 quoted lines
> Could we just kill them all?
> 
> I guess it's a little tricky, because $! is going to give us the pid of
> each subshell. We actually want to kill the test sub-process. That takes
> a few contortions, but the output is nice (every sub-job immediately
> says "ABORTED ...", and the final process does not exit until the whole
> tree is done):
It's trickier than that:
  - Nowadays our test lib translates SIGINT to exit, but not TERM (or
    HUP, in case a user decides to close the terminal window), which
    means that if the test runs any daemons in the background, then
    those won't be killed when the stress test is aborted.
    This is easy enough to address, and I've been meaning to do this
    in an other patch series anyway.
  - With the (implied) '--verbose-log' the process killed in the
    background subshell's SIGTERM handler is not the actual test
    process, because '--verbose-log' runs the test again in a piped
    subshell to save its output:
    
      (GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
       echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
    That 'kill' kills the "outer" shell process.
    And though I get "ABORTED..." immediately and I get back my
    prompt right away, the commands involved in the above construct
    are still running in the background:
      $ ./t3903-stash.sh --stress=1
      ^CABORTED  0.0
      $ ps a |grep t3903
      1280 pts/17   S      0:00 /bin/sh ./t3903-stash.sh --stress=1
      1281 pts/17   S      0:00 tee -a <...>/test-results/t3903-stash.stress-0.out
      1282 pts/17   S      0:00 /bin/sh ./t3903-stash.sh --stress=1
      4173 pts/17   S+     0:00 grep t3903
    I see this behavior with all shells I tried.
    I haven't yet started to think it through why this happens :)
    Not implying '--verbose-log' but redirecting the test script's
    output, like you did in your 'stress' script, seems to work in
    dash, ksh, and ks93, but not in Bash v4.3 or later, where, for
    whatever reason, the subshell get SIGINT before the SIGTERM would
    arrive.
    While we could easily handle SIGINT in the subshell with the same
    signal handler as SIGTERM, it bothers me that something
    fundamental is wrong here.
    Furthermore, then we should perhaps forbid '--stress' in
    combination with '--verbose-log' or '--tee'.
    
Show 50 quoted lines
> diff --git a/t/test-lib.sh b/t/test-lib.sh
> index 9b7f687396..357dead3ff 100644
> --- a/t/test-lib.sh
> +++ b/t/test-lib.sh
> @@ -98,8 +98,9 @@ done,*)
>  	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
>  	stressfail="$TEST_RESULTS_BASE.stress-failed"
>  	rm -f "$stressfail"
> -	trap 'echo aborted >"$stressfail"' TERM INT HUP
> +	trap 'echo aborted >"$stressfail"; kill $job_pids 2>/dev/null; wait' TERM INT HUP
>  
> +	job_pids=
>  	job_nr=0
>  	while test $job_nr -lt "$job_count"
>  	do
> @@ -108,10 +109,13 @@ done,*)
>  			GIT_TEST_STRESS_JOB_NR=$job_nr
>  			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
>  
> +			trap 'kill $test_pid 2>/dev/null' TERM
> +
>  			cnt=0
>  			while ! test -e "$stressfail"
>  			do
> -				if $TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1
> +				$TEST_SHELL_PATH "$0" "$@" >/dev/null 2>&1 & test_pid=$!
> +				if wait
>  				then
>  					printf >&2 "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
>  				elif test -f "$stressfail" &&
> @@ -124,16 +128,11 @@ done,*)
>  				fi
>  				cnt=$(($cnt + 1))
>  			done
> -		) &
> +		) & job_pids="$job_pids $!"
>  		job_nr=$(($job_nr + 1))
>  	done
>  
> -	job_nr=0
> -	while test $job_nr -lt "$job_count"
> -	do
> -		wait
> -		job_nr=$(($job_nr + 1))
> -	done
> +	wait
>  
>  	if test -f "$stressfail" && test "$(cat "$stressfail")" != "aborted"
>  	then
> 
Jeff King· Dec 7, 2018, 01:14 UTC · re: SZEDER Gábor · lore

Re: [RFC PATCH 3/3] test-lib: add the '--stress' option to run a test repeatedly under load

On Fri, Dec 07, 2018 at 12:10:05AM +0100, SZEDER Gábor wrote:
Show 18 quoted lines
> On Wed, Dec 05, 2018 at 04:56:21PM -0500, Jeff King wrote:
> > Could we just kill them all?
> > 
> > I guess it's a little tricky, because $! is going to give us the pid of
> > each subshell. We actually want to kill the test sub-process. That takes
> > a few contortions, but the output is nice (every sub-job immediately
> > says "ABORTED ...", and the final process does not exit until the whole
> > tree is done):
> 
> It's trickier than that:
> 
>   - Nowadays our test lib translates SIGINT to exit, but not TERM (or
>     HUP, in case a user decides to close the terminal window), which
>     means that if the test runs any daemons in the background, then
>     those won't be killed when the stress test is aborted.
> 
>     This is easy enough to address, and I've been meaning to do this
>     in an other patch series anyway.

Yeah, trapping on more signals seems reasonable (or just propagating INT instead of TERM). I'm more worried about this one:

>   - With the (implied) '--verbose-log' the process killed in the
>     background subshell's SIGTERM handler is not the actual test
>     process, because '--verbose-log' runs the test again in a piped
>     subshell to save its output:

Hmm, yeah. The patch I sent earlier already propagates through the subshell, so this is really just another layer there. I.e., would it be reasonable when we are using --verbose-log (or --tee) for the parent process to propagate signals down to child it spawned?

That would fix this case, and it would make things more predictable in general (e.g., a user who manually runs kill).

It does feel like a lot of boilerplate to be propagating these signals manually. I think the "right" Unix-y solution here is process groups: each sub-job should get its own group, and then the top-level runner script can kill the whole group. We may run into portability issues, though (setsid is in util-linux, but I don't know if there's an equivalent elsewhere; a small perl or C helper could do it, but I have no idea what that would mean on Windows).

Show 18 quoted lines
>       (GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
>        echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
> 
>     That 'kill' kills the "outer" shell process.
>     And though I get "ABORTED..." immediately and I get back my
>     prompt right away, the commands involved in the above construct
>     are still running in the background:
> 
>       $ ./t3903-stash.sh --stress=1
>       ^CABORTED  0.0
>       $ ps a |grep t3903
>       1280 pts/17   S      0:00 /bin/sh ./t3903-stash.sh --stress=1
>       1281 pts/17   S      0:00 tee -a <...>/test-results/t3903-stash.stress-0.out
>       1282 pts/17   S      0:00 /bin/sh ./t3903-stash.sh --stress=1
>       4173 pts/17   S+     0:00 grep t3903
> 
>     I see this behavior with all shells I tried.
>     I haven't yet started to think it through why this happens :)

I think you get ABORTED because we are just watching for the parent-process to exit (and reporting its status). The fact that that the subshell (and the tee) are still running is not important. That all makes sense to me.

Show 8 quoted lines
>     Not implying '--verbose-log' but redirecting the test script's
>     output, like you did in your 'stress' script, seems to work in
>     dash, ksh, and ks93, but not in Bash v4.3 or later, where, for
>     whatever reason, the subshell get SIGINT before the SIGTERM would
>     arrive.
>     While we could easily handle SIGINT in the subshell with the same
>     signal handler as SIGTERM, it bothers me that something
>     fundamental is wrong here.

Yeah, I don't understand the SIGINT behavior you're seeing with bash. I can't replicate it with bash 4.4.23 (from Debian unstable).

-Peff
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 0/7] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

This patch series tries to make reproducing rare failures in flaky tests easier: it adds the '--stress' option to our test library to run the test script repeatedly in multiple parallel jobs, in the hope that the increased load creates enough variance in the timing of the test's commands that such a failure is eventually triggered.

Notable changes since v1:
  - Made it more Peff-friendly :), namely it will now show the log of
    the failed test and will rename its trash directory.
    Furthermore, '--stress' will now imply '--verbose -x --immediate'.
  - Improved abort handling based on the discussion of the previous
    version.  (As a result, the patch is so heavily modified, that
    'range-diff' with default parameters consideres it a different
    patch; increasing the creation factor then results in one big ugly
    diff of a diff, so I left it as-is.)
  - Added a few new patches, mostly preparatory refactorings, though
    the first one might be considered a bugfix.
  - Other minor cleanups suggested in the previous discussion.
v1: https://public-inbox.org/git/20181204163457.15717-1-szeder.dev@gmail.com/T/
SZEDER Gábor (7):
  test-lib: translate SIGTERM and SIGHUP to an exit
  test-lib: parse some --options earlier
  test-lib: consolidate naming of test-results paths
  test-lib: set $TRASH_DIRECTORY earlier
  test-lib: extract Bash version check for '-x' tracing
  test-lib-functions: introduce the 'test_set_port' helper function
  test-lib: add the '--stress' option to run a test repeatedly under
    load
 t/README                 |  19 +++-
 t/lib-git-daemon.sh      |   2 +-
 t/lib-git-p4.sh          |   9 +-
 t/lib-git-svn.sh         |   2 +-
 t/lib-httpd.sh           |   2 +-
 t/t0410-partial-clone.sh |   1 -
 t/t5512-ls-remote.sh     |   2 +-
 t/test-lib-functions.sh  |  39 +++++++
 t/test-lib.sh            | 227 +++++++++++++++++++++++++++++----------
 9 files changed, 230 insertions(+), 73 deletions(-)
Range-diff:
-:  ---------- > 1:  3a5c926167 test-lib: translate SIGTERM and SIGHUP to an exit
-:  ---------- > 2:  8eee8d7fba test-lib: parse some --options earlier
1:  f4bb53e676 ! 3:  dd20ae5e9a test-lib: consolidate naming of test-results paths
    @@ -4,8 +4,9 @@
     
         There are two places where we strip off any leading path components
         and the '.sh' suffix from the test script's pathname, and there are
    -    two places where we construct the filename of test output files in
    -    't/test-results/'.  The last patch in this series will add even more.
    +    four places where we construct the name of the 't/test-results'
    +    directory or the name of various test-specific files in there.  The
    +    last patch in this series will add even more.
     
         Factor these out into helper variables to avoid repeating ourselves.
     
    @@ -15,22 +16,23 @@
      --- a/t/test-lib.sh
      +++ b/t/test-lib.sh
     @@
    - 	exit 1
    - fi
    + 	esac
    + done
      
     +TEST_NAME="$(basename "$0" .sh)"
    -+TEST_RESULTS_BASE="$TEST_OUTPUT_DIRECTORY/test-results/$TEST_NAME"
    ++TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
    ++TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
     +
      # if --tee was passed, write the output not only to the terminal, but
      # additionally to the file test-results/$BASENAME.out, too.
    - case "$GIT_TEST_TEE_STARTED, $* " in
    + if test "$GIT_TEST_TEE_STARTED" = "done"
     @@
    - 	# do not redirect again
    - 	;;
    - *' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
    + elif test -n "$tee" || test -n "$verbose_log" ||
    +      test -n "$valgrind" || test -n "$valgrind_only"
    + then
     -	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
     -	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
    -+	mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
    ++	mkdir -p "$TEST_RESULTS_DIR"
      
      	# Make this filename available to the sub-process in case it is using
      	# --verbose-log.
    @@ -48,8 +50,8 @@
     +	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
     +	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
      	exit
    - 	;;
    - esac
    + fi
    + 
     @@
      
      	if test -z "$HARNESS_ACTIVE"
    @@ -58,7 +60,7 @@
     -		mkdir -p "$test_results_dir"
     -		base=${0##*/}
     -		test_results_path="$test_results_dir/${base%.sh}.counts"
    -+		mkdir -p "$(dirname "$TEST_RESULTS_BASE")"
    ++		mkdir -p "$TEST_RESULTS_DIR"
      
     -		cat >"$test_results_path" <<-EOF
     +		cat >"$TEST_RESULTS_BASE.counts" <<-EOF
-:  ---------- > 4:  f3941077e6 test-lib: set $TRASH_DIRECTORY earlier
-:  ---------- > 5:  fefbca96ee test-lib: extract Bash version check for '-x' tracing
2:  9aec8662f9 ! 6:  b4b6844a3e test-lib-functions: introduce the 'test_set_port' helper function
    @@ -27,7 +27,7 @@
             containing the digits 8 or 9 will trigger an error.  Remove all
             leading zeros from the test numbers to prevent this.
     
    -    Note that the Perforce tests are unlike the other tests involving
    +    Note that the 'git p4' tests are unlike the other tests involving
         daemons in that:
     
           - 'lib-git-p4.sh' doesn't use the test's number for unique port as
    @@ -35,7 +35,7 @@
     
           - The port is not overridable via an environment variable.
     
    -    With this patch even Perforce tests will use the test's number as
    +    With this patch even 'git p4' tests will use the test's number as
         default port, and it will be overridable via the P4DPORT environment
         variable.
     
    @@ -161,7 +161,7 @@
     +		BUG "test_set_port requires a variable name"
     +	fi
     +
    -+	eval port=\"\${$var}\"
    ++	eval port=\$$var
     +	case "$port" in
     +	"")
     +		# No port is set in the given env var, use the test
3:  a5aa71f20c < -:  ---------- test-lib: add the '--stress' option to run a test repeatedly under load
-:  ---------- > 7:  8470b55f65 test-lib: add the '--stress' option to run a test repeatedly under load
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 2/7] test-lib: parse some --options earlier

'test-lib.sh' looks for the presence of certain options like '--tee' and '--verbose-log', so it can execute the test script again to save its standard output and error. This happens way before the actual option parsing loop, and the condition looking for these options looks a bit odd, too. This patch series will add two more options to look out for, and, in addition, will have to extract these options' stuck arguments (i.e. '--opt=arg') as well.

Add a proper option parsing loop to check these select options early in 'test-lib.sh', making this early option checking more readable and keeping those later changes in this series simpler. Use a 'for opt in "$@"' loop to iterate over the options to preserve "$@" intact, so options like '--verbose-log' can execute the test script again with all the original options.

As an alternative, we could parse all options early, but there are options that do require an _unstuck_ argument, which is tricky to handle properly in such a for loop, and the resulting complexity is, in my opinion, worse than having this extra, partial option parsing loop.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 53 +++++++++++++++++++++++++++++++--------------------
 1 file changed, 32 insertions(+), 21 deletions(-)
Show changes to t/test-lib.sh +32 −21
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 9a3f7930a3..5577d9dc5a 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -71,13 +71,33 @@ then
 	exit 1
 fi
 
+# Parse some options early, taking care to leave $@ intact.
+for opt
+do
+	case "$opt" in
+	--tee)
+		tee=t ;;
+	-V|--verbose-log)
+		verbose_log=t ;;
+	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
+		valgrind=memcheck ;;
+	--valgrind=*)
+		valgrind=${opt#--*=} ;;
+	--valgrind-only=*)
+		valgrind_only=${opt#--*=} ;;
+	*)
+		# Other options will be handled later.
+	esac
+done
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
-case "$GIT_TEST_TEE_STARTED, $* " in
-done,*)
-	# do not redirect again
-	;;
-*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
+if test "$GIT_TEST_TEE_STARTED" = "done"
+then
+	: # do not redirect again
+elif test -n "$tee" || test -n "$verbose_log" ||
+     test -n "$valgrind" || test -n "$valgrind_only"
+then
 	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
 	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
 
@@ -94,8 +114,7 @@ done,*)
 	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
 	test "$(cat "$BASE.exit")" = 0
 	exit
-	;;
-esac
+fi
 
 # For repeatability, reset the environment to known value.
 # TERM is sanitized below, after saving color control sequences.
@@ -296,17 +315,6 @@ do
 		with_dashes=t; shift ;;
 	--no-color)
 		color=; shift ;;
-	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
-		valgrind=memcheck
-		shift ;;
-	--valgrind=*)
-		valgrind=${1#--*=}
-		shift ;;
-	--valgrind-only=*)
-		valgrind_only=${1#--*=}
-		shift ;;
-	--tee)
-		shift ;; # was handled already
 	--root=*)
 		root=${1#--*=}
 		shift ;;
@@ -336,9 +344,12 @@ do
 			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
 		fi
 		shift ;;
-	-V|--verbose-log)
-		verbose_log=t
-		shift ;;
+	--tee|\
+	-V|--verbose-log|\
+	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
+	--valgrind=*|\
+	--valgrind-only=*)
+		shift ;; # These options were handled already.
 	*)
 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
 	esac
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Jeff King· Dec 11, 2018, 11:09 UTC · re: SZEDER Gábor · lore

Re: [PATCH v2 2/7] test-lib: parse some --options earlier

On Sun, Dec 09, 2018 at 11:56:23PM +0100, SZEDER Gábor wrote:
Show 20 quoted lines
> 'test-lib.sh' looks for the presence of certain options like '--tee'
> and '--verbose-log', so it can execute the test script again to save
> its standard output and error.  This happens way before the actual
> option parsing loop, and the condition looking for these options looks
> a bit odd, too.  This patch series will add two more options to look
> out for, and, in addition, will have to extract these options' stuck
> arguments (i.e. '--opt=arg') as well.
> 
> Add a proper option parsing loop to check these select options early
> in 'test-lib.sh', making this early option checking more readable and
> keeping those later changes in this series simpler.  Use a 'for opt in
> "$@"' loop to iterate over the options to preserve "$@" intact, so
> options like '--verbose-log' can execute the test script again with
> all the original options.
> 
> As an alternative, we could parse all options early, but there are
> options that do require an _unstuck_ argument, which is tricky to
> handle properly in such a for loop, and the resulting complexity is,
> in my opinion, worse than having this extra, partial option parsing
> loop.

In general, I'm not wild about having multiple option-parsing loops that skip the normal left-to-right parsing, since it introduces funny corner cases (like "-foo --bar" which should be the same as "--foo=--bar" instead thinking that "--bar" was passed as an option).

But looking at what this is replacing:
Show 5 quoted lines
> -case "$GIT_TEST_TEE_STARTED, $* " in
> -done,*)
> -	# do not redirect again
> -	;;
> -*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
your version is easily an order of magnitude less horrible. ;)
>  t/test-lib.sh | 53 +++++++++++++++++++++++++++++++--------------------
>  1 file changed, 32 insertions(+), 21 deletions(-)
This looks good to me overall, though...
Show 21 quoted lines
> +# Parse some options early, taking care to leave $@ intact.
> +for opt
> +do
> +	case "$opt" in
> +	--tee)
> +		tee=t ;;
> +	-V|--verbose-log)
> +		verbose_log=t ;;
> +	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
> +		valgrind=memcheck ;;
> +	--valgrind=*)
> +		valgrind=${opt#--*=} ;;
> +	--valgrind-only=*)
> +		valgrind_only=${opt#--*=} ;;
> +	*)
> +		# Other options will be handled later.
> +	esac
> +done
> [...]
> +elif test -n "$tee" || test -n "$verbose_log" ||
> +     test -n "$valgrind" || test -n "$valgrind_only"

Now that we've nicely moved the parsing up, would it make sense to put the annotation for "this option implies --tee" with those options?

I.e., set tee=t when we see --verbose-log, which keeps all of the verbose-log logic together?

Show 14 quoted lines
> @@ -336,9 +344,12 @@ do
>  			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
>  		fi
>  		shift ;;
> -	-V|--verbose-log)
> -		verbose_log=t
> -		shift ;;
> +	--tee|\
> +	-V|--verbose-log|\
> +	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
> +	--valgrind=*|\
> +	--valgrind-only=*)
> +		shift ;; # These options were handled already.
>  	*)

It's too bad there's not an easy way to selectively remove from the $@ array (which would avoid duplicating this list here).

The best I could come up with is:
-- >8 --
first=t
for i in "$@"; do
	test -n "$first" && set --
	first=
	case "$i" in
	--foo)
		echo "saw foo" ;;
	*)
		set -- "$@" "$i" ;;
	esac
done
for i in "$@"; do
	echo "remainder: $i"
done
-- 8< --

but I won't be surprised if there are portability problems with assigning $@ in the middle of a loop that iterates over it.

-Peff
SZEDER Gábor· Dec 11, 2018, 12:42 UTC · re: Jeff King · lore

Re: [PATCH v2 2/7] test-lib: parse some --options earlier

On Tue, Dec 11, 2018 at 06:09:19AM -0500, Jeff King wrote:
Show 27 quoted lines
> On Sun, Dec 09, 2018 at 11:56:23PM +0100, SZEDER Gábor wrote:
> 
> > 'test-lib.sh' looks for the presence of certain options like '--tee'
> > and '--verbose-log', so it can execute the test script again to save
> > its standard output and error.  This happens way before the actual
> > option parsing loop, and the condition looking for these options looks
> > a bit odd, too.  This patch series will add two more options to look
> > out for, and, in addition, will have to extract these options' stuck
> > arguments (i.e. '--opt=arg') as well.
> > 
> > Add a proper option parsing loop to check these select options early
> > in 'test-lib.sh', making this early option checking more readable and
> > keeping those later changes in this series simpler.  Use a 'for opt in
> > "$@"' loop to iterate over the options to preserve "$@" intact, so
> > options like '--verbose-log' can execute the test script again with
> > all the original options.
> > 
> > As an alternative, we could parse all options early, but there are
> > options that do require an _unstuck_ argument, which is tricky to
> > handle properly in such a for loop, and the resulting complexity is,
> > in my opinion, worse than having this extra, partial option parsing
> > loop.
> 
> In general, I'm not wild about having multiple option-parsing loops that
> skip the normal left-to-right parsing, since it introduces funny corner
> cases (like "-foo --bar" which should be the same as "--foo=--bar"
> instead thinking that "--bar" was passed as an option).

Yeah, that's already an "issue" in the current implementation as well, though there are no such options that require options as argument.

Show 7 quoted lines
> But looking at what this is replacing:
> 
> > -case "$GIT_TEST_TEE_STARTED, $* " in
> > -done,*)
> > -	# do not redirect again
> > -	;;
> > -*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)

Anyway, I had another crack at turning the current option parsing loop into a for loop keeping $@ intact, and the results don't look all that bad this time. Note that this diff below only does the while -> for conversion, but leaves the loop where it is, so the changes are easily visible. The important bits are the conditions at the beginning of the loop and after the loop, and the handling of '-r'; the rest is mostly s/shift// and sort-of s/$1/$opt/.

Thoughts?  Is it better than two loops?  I think it's better.
Show changes to t/test-lib.sh +44 −31
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 9a3f7930a3..efdb6be3c8 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -264,58 +264,65 @@ test "x$TERM" != "xdumb" && (
 	) &&
 	color=t
 
-while test "$#" -ne 0
+store_arg_to=
+prev_opt=
+for opt
 do
-	case "$1" in
+	if test -n "$store_arg_to"
+	then
+		eval $store_arg_to=\$opt
+		store_arg_to=
+		prev_opt=
+		continue
+	fi
+	case "$opt" in
 	-d|--d|--de|--deb|--debu|--debug)
-		debug=t; shift ;;
+		debug=t ;;
 	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
-		immediate=t; shift ;;
+		immediate=t ;;
 	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG; shift ;;
+		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
 	-r)
-		shift; test "$#" -ne 0 || {
-			echo 'error: -r requires an argument' >&2;
-			exit 1;
-		}
-		run_list=$1; shift ;;
+		store_arg_to=run_list
+		prev_opt=-r
+		;;
 	--run=*)
-		run_list=${1#--*=}; shift ;;
+		run_list=${opt#--*=} ;;
 	-h|--h|--he|--hel|--help)
-		help=t; shift ;;
+		help=t ;;
 	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
-		verbose=t; shift ;;
+		verbose=t ;;
 	--verbose-only=*)
-		verbose_only=${1#--*=}
-		shift ;;
+		verbose_only=${opt#--*=}
+		;;
 	-q|--q|--qu|--qui|--quie|--quiet)
 		# Ignore --quiet under a TAP::Harness. Saying how many tests
 		# passed without the ok/not ok details is always an error.
-		test -z "$HARNESS_ACTIVE" && quiet=t; shift ;;
+		test -z "$HARNESS_ACTIVE" && quiet=t ;;
 	--with-dashes)
-		with_dashes=t; shift ;;
+		with_dashes=t ;;
 	--no-color)
-		color=; shift ;;
+		color= ;;
 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
 		valgrind=memcheck
-		shift ;;
+		;;
 	--valgrind=*)
-		valgrind=${1#--*=}
-		shift ;;
+		valgrind=${opt#--*=}
+		;;
 	--valgrind-only=*)
-		valgrind_only=${1#--*=}
-		shift ;;
+		valgrind_only=${opt#--*=}
+		;;
 	--tee)
-		shift ;; # was handled already
+		;; # was handled already
 	--root=*)
-		root=${1#--*=}
-		shift ;;
+		root=${opt#--*=}
+		;;
 	--chain-lint)
 		GIT_TEST_CHAIN_LINT=1
-		shift ;;
+		;;
 	--no-chain-lint)
 		GIT_TEST_CHAIN_LINT=0
-		shift ;;
+		;;
 	-x)
 		# Some test scripts can't be reliably traced  with '-x',
 		# unless the test is run with a Bash version supporting
@@ -335,15 +342,21 @@ do
 		else
 			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
 		fi
-		shift ;;
+		;;
 	-V|--verbose-log)
 		verbose_log=t
-		shift ;;
+		;;
 	*)
-		echo "error: unknown test option '$1'" >&2; exit 1 ;;
+		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
 	esac
 done
 
+if test -n "$store_arg_to"
+then
+	echo "error: $prev_opt requires an argument" >&2
+	exit 1
+fi
+
 if test -n "$valgrind_only"
 then
 	test -z "$valgrind" && valgrind=memcheck
Jeff King· Dec 17, 2018, 21:44 UTC · re: SZEDER Gábor · lore

Re: [PATCH v2 2/7] test-lib: parse some --options earlier

On Tue, Dec 11, 2018 at 01:42:45PM +0100, SZEDER Gábor wrote:
Show 18 quoted lines
> > But looking at what this is replacing:
> > 
> > > -case "$GIT_TEST_TEE_STARTED, $* " in
> > > -done,*)
> > > -	# do not redirect again
> > > -	;;
> > > -*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
> 
> 
> Anyway, I had another crack at turning the current option parsing loop
> into a for loop keeping $@ intact, and the results don't look all that
> bad this time.  Note that this diff below only does the while -> for
> conversion, but leaves the loop where it is, so the changes are easily
> visible.  The important bits are the conditions at the beginning of
> the loop and after the loop, and the handling of '-r'; the rest is
> mostly s/shift// and sort-of s/$1/$opt/.
> 
> Thoughts?  Is it better than two loops?  I think it's better.

It certainly looks better to me. It also makes sense to me to validate the options before forking/logging, though I suppose one could argue the opposite.

I wonder why we didn't do it this way in the beginning (i.e., why the tee bits were all handled separately before the parsing phase). I guess just because we have to pass the options down to the sub-process.

Show 21 quoted lines
> diff --git a/t/test-lib.sh b/t/test-lib.sh
> index 9a3f7930a3..efdb6be3c8 100644
> --- a/t/test-lib.sh
> +++ b/t/test-lib.sh
> @@ -264,58 +264,65 @@ test "x$TERM" != "xdumb" && (
>  	) &&
>  	color=t
>  
> -while test "$#" -ne 0
> +store_arg_to=
> +prev_opt=
> +for opt
>  do
> -	case "$1" in
> +	if test -n "$store_arg_to"
> +	then
> +		eval $store_arg_to=\$opt
> +		store_arg_to=
> +		prev_opt=
> +		continue
> +	fi

OK, so this is set for the unstuck options, which then pick up the option in the next loop iteration. That's perhaps less gross than my "re-build the options with set --" trick.

A simple variable set is enough for "-r". In theory we could make this:
  if test -n "$handle_unstuck_arg"
  then
	eval "$handle_unstuck_arg \$1"
  fi
  ...
  -r)
	handle_unstuck_arg=handle_opt_r ;;

and handle_opt_r() could do whatever it wants. But I don't really foresee us adding a lot of new options (in fact, given that this is just the internal tests, I am tempted to say that we should just make it "-r<arg>" for the sake of simplicity and consistency. But maybe somebody would be annoyed. I have never used "-r" ever myself).

-Peff
SZEDER Gábor· Dec 30, 2018, 19:04 UTC · re: Jeff King · lore

Re: [PATCH v2 2/7] test-lib: parse some --options earlier

On Mon, Dec 17, 2018 at 04:44:36PM -0500, Jeff King wrote:
> On Tue, Dec 11, 2018 at 01:42:45PM +0100, SZEDER Gábor wrote:
Show 39 quoted lines
> > diff --git a/t/test-lib.sh b/t/test-lib.sh
> > index 9a3f7930a3..efdb6be3c8 100644
> > --- a/t/test-lib.sh
> > +++ b/t/test-lib.sh
> > @@ -264,58 +264,65 @@ test "x$TERM" != "xdumb" && (
> >  	) &&
> >  	color=t
> >  
> > -while test "$#" -ne 0
> > +store_arg_to=
> > +prev_opt=
> > +for opt
> >  do
> > -	case "$1" in
> > +	if test -n "$store_arg_to"
> > +	then
> > +		eval $store_arg_to=\$opt
> > +		store_arg_to=
> > +		prev_opt=
> > +		continue
> > +	fi
> 
> OK, so this is set for the unstuck options, which then pick up the
> option in the next loop iteration. That's perhaps less gross than my
> "re-build the options with set --" trick.
> 
> A simple variable set is enough for "-r". In theory we could make this:
> 
>   if test -n "$handle_unstuck_arg"
>   then
> 	eval "$handle_unstuck_arg \$1"
>   fi
>   ...
> 
>   -r)
> 	handle_unstuck_arg=handle_opt_r ;;
> 
> and handle_opt_r() could do whatever it wants. But I don't really
> foresee us adding a lot of new options

Yeah, I would refrain from making it too general and fancy with a callback function for now, when there is only a single option that could use it.

> (in fact, given that this is just
> the internal tests, I am tempted to say that we should just make it
> "-r<arg>" for the sake of simplicity and consistency. But maybe somebody
> would be annoyed. I have never used "-r" ever myself).
I didn't even know what '-r' does...

And I agree that changing it to '-r<arg>' would be the best, but this patch series is about adding '--stress', so changing how '-r' gets its mandatory argument (and potentially annoying someone) is beyond the scope, I would say.

Jeff King· Jan 3, 2019, 04:53 UTC · re: SZEDER Gábor · lore

Re: [PATCH v2 2/7] test-lib: parse some --options earlier

On Sun, Dec 30, 2018 at 08:04:19PM +0100, SZEDER Gábor wrote:
Show 6 quoted lines
> > (in fact, given that this is just
> > the internal tests, I am tempted to say that we should just make it
> > "-r<arg>" for the sake of simplicity and consistency. But maybe somebody
> > would be annoyed. I have never used "-r" ever myself).
> 
> I didn't even know what '-r' does...
I had to look it up, too. :)
> And I agree that changing it to '-r<arg>' would be the best, but this
> patch series is about adding '--stress', so changing how '-r' gets its
> mandatory argument (and potentially annoying someone) is beyond the
> scope, I would say.

OK, I'm fine with that (though once we've built the infrastructure to handle its unstuck form, I don't know if there's much point in changing it, so we can probably just let it live on forever).

-Peff
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 1/7] test-lib: translate SIGTERM and SIGHUP to an exit

Right now if a test script receives SIGTERM or SIGHUP (e.g., because a test was hanging and the user 'kill'-ed it or simply closed the terminal window the test was running in), the shell exits immediately. This can be annoying if the test script did any global setup, like starting apache or git-daemon, as it will not have an opportunity to clean up after itself. A subsequent run of the test won't be able to start its own daemon, and will either fail or skip the tests.

Instead, let's trap SIGTERM and SIGHUP as well to make sure we do a clean shutdown, and just chain it to a normal exit (which will trigger any cleanup).

This patch follows suit of da706545f7 (t: translate SIGINT to an exit, 2015-03-13), and even stole its commit message as well.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to t/test-lib.sh +1 −1
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 0f1faa24b2..9a3f7930a3 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -476,7 +476,7 @@ die () {
 
 GIT_EXIT_OK=
 trap 'die' EXIT
-trap 'exit $?' INT
+trap 'exit $?' INT TERM HUP
 
 # The user-facing functions are loaded from a separate file so that
 # test_perf subshells can have them too
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Jeff King· Dec 11, 2018, 10:57 UTC · re: SZEDER Gábor · lore

Re: [PATCH v2 1/7] test-lib: translate SIGTERM and SIGHUP to an exit

On Sun, Dec 09, 2018 at 11:56:22PM +0100, SZEDER Gábor wrote:
Show 14 quoted lines
> Right now if a test script receives SIGTERM or SIGHUP (e.g., because a
> test was hanging and the user 'kill'-ed it or simply closed the
> terminal window the test was running in), the shell exits immediately.
> This can be annoying if the test script did any global setup, like
> starting apache or git-daemon, as it will not have an opportunity to
> clean up after itself. A subsequent run of the test won't be able to
> start its own daemon, and will either fail or skip the tests.
> 
> Instead, let's trap SIGTERM and SIGHUP as well to make sure we do a
> clean shutdown, and just chain it to a normal exit (which will trigger
> any cleanup).
> 
> This patch follows suit of da706545f7 (t: translate SIGINT to an exit,
> 2015-03-13), and even stole its commit message as well.
No wonder it was so nicely explained. ;)

I think this is quite a reasonable thing to do. Since we're trying to clean up, in theory we would like to hook any signal death, but these three are the common ones in practice. We handle QUIT and PIPE as well in our C code; the latter isn't an issue here. SIGQUIT is a possibility, I guess, but seems rather unlikely.

-Peff
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 4/7] test-lib: set $TRASH_DIRECTORY earlier

A later patch in this series will need to know the path to the trash directory early in 'test-lib.sh', but $TRASH_DIRECTORY is set much later. Furthermore, the path to the trash directory depends on the '--root=<path>' option, which, too, is parsed too late.

Move parsing '--root=...' to the early option parsing loop, and set $TRASH_DIRECTORY where the other test-specific path variables are set.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)
Show changes to t/test-lib.sh +10 −10
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 09c77cbd1b..ea1cd34013 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -85,6 +85,8 @@ do
 		valgrind=${opt#--*=} ;;
 	--valgrind-only=*)
 		valgrind_only=${opt#--*=} ;;
+	--root=*)
+		root=${opt#--*=} ;;
 	*)
 		# Other options will be handled later.
 	esac
@@ -93,6 +95,12 @@ done
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
 TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
+test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
+case "$TRASH_DIRECTORY" in
+/*) ;; # absolute path is good
+ *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
+esac
 
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
@@ -318,9 +326,6 @@ do
 		with_dashes=t; shift ;;
 	--no-color)
 		color=; shift ;;
-	--root=*)
-		root=${1#--*=}
-		shift ;;
 	--chain-lint)
 		GIT_TEST_CHAIN_LINT=1
 		shift ;;
@@ -351,7 +356,8 @@ do
 	-V|--verbose-log|\
 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
 	--valgrind=*|\
-	--valgrind-only=*)
+	--valgrind-only=*|\
+	--root=*)
 		shift ;; # These options were handled already.
 	*)
 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
@@ -1040,12 +1046,6 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
-test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
-case "$TRASH_DIRECTORY" in
-/*) ;; # absolute path is good
- *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
-esac
 rm -fr "$TRASH_DIRECTORY" || {
 	GIT_EXIT_OK=t
 	echo >&5 "FATAL: Cannot prepare test area"
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 5/7] test-lib: extract Bash version check for '-x' tracing

Some of our test scripts can't be reliably run with '-x' tracing enabled unless executed with a Bash version supporting BASH_XTRACEFD (since v4.1), and we have a lengthy condition to disable tracing if such a test script is not executed with a suitable Bash version.

Move this check out from the option parsing loop, so other options can imply '-x' by setting 'trace=t', without missing this Bash version check.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)
Show changes to t/test-lib.sh +19 −18
diff --git a/t/test-lib.sh b/t/test-lib.sh
index ea1cd34013..d55d158580 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -333,24 +333,7 @@ do
 		GIT_TEST_CHAIN_LINT=0
 		shift ;;
 	-x)
-		# Some test scripts can't be reliably traced  with '-x',
-		# unless the test is run with a Bash version supporting
-		# BASH_XTRACEFD (introduced in Bash v4.1).  Check whether
-		# this test is marked as such, and ignore '-x' if it
-		# isn't executed with a suitable Bash version.
-		if test -z "$test_untraceable" || {
-		     test -n "$BASH_VERSION" && {
-		       test ${BASH_VERSINFO[0]} -gt 4 || {
-			 test ${BASH_VERSINFO[0]} -eq 4 &&
-			 test ${BASH_VERSINFO[1]} -ge 1
-		       }
-		     }
-		   }
-		then
-			trace=t
-		else
-			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		fi
+		trace=t
 		shift ;;
 	--tee|\
 	-V|--verbose-log|\
@@ -373,6 +356,24 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$trace" && test -n "$test_untraceable"
+then
+	# '-x' tracing requested, but this test script can't be reliably
+	# traced, unless it is run with a Bash version supporting
+	# BASH_XTRACEFD (introduced in Bash v4.1).
+	if test -n "$BASH_VERSION" && {
+	     test ${BASH_VERSINFO[0]} -gt 4 || {
+	       test ${BASH_VERSINFO[0]} -eq 4 &&
+	       test ${BASH_VERSINFO[1]} -ge 1
+	     }
+	   }
+	then
+		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
+	else
+		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		trace=
+	fi
+fi
 if test -n "$trace" && test -z "$verbose_log"
 then
 	verbose=t
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 3/7] test-lib: consolidate naming of test-results paths

There are two places where we strip off any leading path components and the '.sh' suffix from the test script's pathname, and there are four places where we construct the name of the 't/test-results' directory or the name of various test-specific files in there. The last patch in this series will add even more.

Factor these out into helper variables to avoid repeating ourselves.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
Show changes to t/test-lib.sh +11 −11
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 5577d9dc5a..09c77cbd1b 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -90,6 +90,10 @@ do
 	esac
 done
 
+TEST_NAME="$(basename "$0" .sh)"
+TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
@@ -98,12 +102,11 @@ then
 elif test -n "$tee" || test -n "$verbose_log" ||
      test -n "$valgrind" || test -n "$valgrind_only"
 then
-	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
-	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
+	mkdir -p "$TEST_RESULTS_DIR"
 
 	# Make this filename available to the sub-process in case it is using
 	# --verbose-log.
-	GIT_TEST_TEE_OUTPUT_FILE=$BASE.out
+	GIT_TEST_TEE_OUTPUT_FILE=$TEST_RESULTS_BASE.out
 	export GIT_TEST_TEE_OUTPUT_FILE
 
 	# Truncate before calling "tee -a" to get rid of the results
@@ -111,8 +114,8 @@ then
 	>"$GIT_TEST_TEE_OUTPUT_FILE"
 
 	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
-	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
-	test "$(cat "$BASE.exit")" = 0
+	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
+	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
 	exit
 fi
 
@@ -829,12 +832,9 @@ test_done () {
 
 	if test -z "$HARNESS_ACTIVE"
 	then
-		test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results"
-		mkdir -p "$test_results_dir"
-		base=${0##*/}
-		test_results_path="$test_results_dir/${base%.sh}.counts"
+		mkdir -p "$TEST_RESULTS_DIR"
 
-		cat >"$test_results_path" <<-EOF
+		cat >"$TEST_RESULTS_BASE.counts" <<-EOF
 		total $test_count
 		success $test_success
 		fixed $test_fixed
@@ -1040,7 +1040,7 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$(basename "$0" .sh)"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 6/7] test-lib-functions: introduce the 'test_set_port' helper function

Several test scripts run daemons like 'git-daemon' or Apache, and communicate with them through TCP sockets. To have unique ports where these daemons are accessible, the ports are usually the number of the corresponding test scripts, unless the user overrides them via environment variables, and thus all those tests and test libs contain more or less the same bit of one-liner boilerplate code to find out the port. The last patch in this series will make this a bit more complicated.

Factor out finding the port for a daemon into the common helper function 'test_set_port' to avoid repeating ourselves.

Take special care of test scripts with "low" numbers:
  - Test numbers below 1024 would result in a port that's only usable
    as root, so set their port to '10000 + test-nr' to make sure it
    doesn't interfere with other tests in the test suite.  This makes
    the hardcoded port number in 't0410-partial-clone.sh' unnecessary,
    remove it.
  - The shell's arithmetic evaluation interprets numbers with leading
    zeros as octal values, which means that test number below 1000 and
    containing the digits 8 or 9 will trigger an error.  Remove all
    leading zeros from the test numbers to prevent this.

Note that the 'git p4' tests are unlike the other tests involving daemons in that:

  - 'lib-git-p4.sh' doesn't use the test's number for unique port as
    is, but does a bit of additional arithmetic on top [1].
  - The port is not overridable via an environment variable.

With this patch even 'git p4' tests will use the test's number as default port, and it will be overridable via the P4DPORT environment variable.

[1] Commit fc00233071 (git-p4 tests: refactor and cleanup, 2011-08-22)
    introduced that "unusual" unique port computation without
    explaining why it was necessary (as opposed to simply using the
    test number as is).  It seems to be just unnecessary complication,
    and in any case that commit came way before the "test nr as unique
    port" got "standardized" for other daemons in commits c44132fcf3
    (tests: auto-set git-daemon port, 2014-02-10), 3bb486e439 (tests:
    auto-set LIB_HTTPD_PORT from test name, 2014-02-10), and
    bf9d7df950 (t/lib-git-svn.sh: improve svnserve tests with parallel
    make test, 2017-12-01).
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/lib-git-daemon.sh      |  2 +-
 t/lib-git-p4.sh          |  9 +--------
 t/lib-git-svn.sh         |  2 +-
 t/lib-httpd.sh           |  2 +-
 t/t0410-partial-clone.sh |  1 -
 t/t5512-ls-remote.sh     |  2 +-
 t/test-lib-functions.sh  | 36 ++++++++++++++++++++++++++++++++++++
 7 files changed, 41 insertions(+), 13 deletions(-)
Show changes to 7 files +41 −13

t/lib-git-daemon.sh, t/lib-git-p4.sh, t/lib-git-svn.sh, t/lib-httpd.sh, t/t0410-partial-clone.sh, t/t5512-ls-remote.sh, t/test-lib-functions.sh

diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh
index f98de95c15..41eb1e3ae8 100644
--- a/t/lib-git-daemon.sh
+++ b/t/lib-git-daemon.sh
@@ -28,7 +28,7 @@ then
 	test_skip_or_die $GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
 fi
 
-LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-${this_test#t}}
+test_set_port LIB_GIT_DAEMON_PORT
 
 GIT_DAEMON_PID=
 GIT_DAEMON_DOCUMENT_ROOT_PATH="$PWD"/repo
diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh
index c27599474c..b3be3ba011 100644
--- a/t/lib-git-p4.sh
+++ b/t/lib-git-p4.sh
@@ -53,14 +53,7 @@ time_in_seconds () {
 	(cd / && "$PYTHON_PATH" -c 'import time; print(int(time.time()))')
 }
 
-# Try to pick a unique port: guess a large number, then hope
-# no more than one of each test is running.
-#
-# This does not handle the case where somebody else is running the
-# same tests and has chosen the same ports.
-testid=${this_test#t}
-git_p4_test_start=9800
-P4DPORT=$((10669 + ($testid - $git_p4_test_start)))
+test_set_port P4DPORT
 
 P4PORT=localhost:$P4DPORT
 P4CLIENT=client
diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
index a8130f9119..f3b478c307 100644
--- a/t/lib-git-svn.sh
+++ b/t/lib-git-svn.sh
@@ -13,6 +13,7 @@ fi
 GIT_DIR=$PWD/.git
 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
@@ -119,7 +120,6 @@ require_svnserve () {
 }
 
 start_svnserve () {
-	SVNSERVE_PORT=${SVNSERVE_PORT-${this_test#t}}
 	svnserve --listen-port $SVNSERVE_PORT \
 		 --root "$rawsvnrepo" \
 		 --listen-once \
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index a8729f8232..e465116ef9 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -82,7 +82,7 @@ case $(uname) in
 esac
 
 LIB_HTTPD_PATH=${LIB_HTTPD_PATH-"$DEFAULT_HTTPD_PATH"}
-LIB_HTTPD_PORT=${LIB_HTTPD_PORT-${this_test#t}}
+test_set_port LIB_HTTPD_PORT
 
 TEST_PATH="$TEST_DIRECTORY"/lib-httpd
 HTTPD_ROOT_PATH="$PWD"/httpd
diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh
index ba3887f178..0aca8d7588 100755
--- a/t/t0410-partial-clone.sh
+++ b/t/t0410-partial-clone.sh
@@ -480,7 +480,6 @@ test_expect_success 'gc stops traversal when a missing but promised object is re
 	! grep "$TREE_HASH" out
 '
 
-LIB_HTTPD_PORT=12345  # default port, 410, cannot be used as non-root
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 32e722db2e..cd9e60632d 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -260,7 +260,7 @@ test_lazy_prereq GIT_DAEMON '
 # This test spawns a daemon, so run it only if the user would be OK with
 # testing with git-daemon.
 test_expect_success PIPE,JGIT,GIT_DAEMON 'indicate no refs in standards-compliant empty remote' '
-	JGIT_DAEMON_PORT=${JGIT_DAEMON_PORT-${this_test#t}} &&
+	test_set_port JGIT_DAEMON_PORT &&
 	JGIT_DAEMON_PID= &&
 	git init --bare empty.git &&
 	>empty.git/git-daemon-export-ok &&
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 6b3bbf99e4..3746327bde 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1263,3 +1263,39 @@ test_oid () {
 	fi &&
 	eval "printf '%s' \"\${$var}\""
 }
+
+# Choose a port number based on the test script's number and store it in
+# the given variable name, unless that variable already contains a number.
+test_set_port () {
+	local var=$1 port
+
+	if test $# -ne 1 || test -z "$var"
+	then
+		BUG "test_set_port requires a variable name"
+	fi
+
+	eval port=\$$var
+	case "$port" in
+	"")
+		# No port is set in the given env var, use the test
+		# number as port number instead.
+		# Remove not only the leading 't', but all leading zeros
+		# as well, so the arithmetic below won't (mis)interpret
+		# a test number like '0123' as an octal value.
+		port=${this_test#${this_test%%[1-9]*}}
+		if test "${port:-0}" -lt 1024
+		then
+			# root-only port, use a larger one instead.
+			port=$(($port + 10000))
+		fi
+
+		eval $var=$port
+		;;
+	*[^0-9]*)
+		error >&7 "invalid port number: $port"
+		;;
+	*)
+		# The user has specified the port.
+		;;
+	esac
+}
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 9, 2018, 22:56 UTC · re: SZEDER Gábor · lore

[PATCH v2 7/7] test-lib: add the '--stress' option to run a test repeatedly under load

Unfortunately, we have a few flaky tests, whose failures tend to be hard to reproduce. We've found that the best we can do to reproduce such a failure is to run the test repeatedly while the machine is under load, and wait in the hope that the load creates enough variance in the timing of the test's commands that a failure is evenually triggered. I have a command to do that, and I noticed that two other contributors have rolled their own scripts to do the same, all choosing slightly different approaches.

To help reproduce failures in flaky tests, introduce the '--stress' option to run a test script repeatedly in multiple parallel jobs until one of them fails, thereby using the test script itself to increase the load on the machine.

The number of parallel jobs is determined by, in order of precedence: the number specified as '--stress=<N>', or the value of the GIT_TEST_STRESS_LOAD environment variable, or twice the number of available processors (as reported by the 'getconf' utility), or 8.

Make '--stress' imply '--verbose -x --immediate' to get the most information about rare failures; there is really no point in spending all the extra effort to reproduce such a failure, and then not know which command failed and why.

To prevent the several parallel invocations of the same test from interfering with each other:

  - Include the parallel job's number in the name of the trash
    directory and the various output files under 't/test-results/' as
    a '.stress-<Nr>' suffix.
  - Add the parallel job's number to the port number specified by the
    user or to the test number, so even tests involving daemons
    listening on a TCP socket can be stressed.
  - Redirect each parallel test run's output to
    't/test-results/$TEST_NAME.stress-<nr>.out', because dumping the
    output of several parallel running tests to the terminal would
    create a big ugly mess.

For convenience, print the output of the failed test job at the end, and rename its trash directory to end with the '.stress-failed' suffix, so it's easy to find in a predictable path. If, in an unlikely case, more than one jobs were to fail nearly at the same time, then print the output of all failed jobs, and rename the trash directory of only the last one (i.e. with the highest job number), as it is the trash directory of the test whose output will be at the bottom of the user's terminal.

Based on Jeff King's 'stress' script.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/README                |  19 +++++++-
 t/test-lib-functions.sh |   7 ++-
 t/test-lib.sh           | 103 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 123 insertions(+), 6 deletions(-)
Show changes to 3 files +123 −6

t/README, t/test-lib-functions.sh, t/test-lib.sh

diff --git a/t/README b/t/README
index 28711cc508..11ce7675e3 100644
--- a/t/README
+++ b/t/README
@@ -186,6 +186,22 @@ appropriately before running "make".
 	this feature by setting the GIT_TEST_CHAIN_LINT environment
 	variable to "1" or "0", respectively.
 
+--stress::
+--stress=<N>::
+	Run the test script repeatedly in multiple parallel jobs until
+	one of them fails.  Useful for reproducing rare failures in
+	flaky tests.  The number of parallel jobs is, in order of
+	precedence: <N>, or the value of the GIT_TEST_STRESS_LOAD
+	environment variable, or twice the number of available
+	processors (as shown by the 'getconf' utility),	or 8.
+	Implies `--verbose -x --immediate` to get the most information
+	about the failure.  Note that the verbose output of each test
+	job is saved to 't/test-results/$TEST_NAME.stress-<nr>.out',
+	and only the output of the failed test job is shown on the
+	terminal.  The names of the trash directories get a
+	'.stress-<nr>' suffix, and the trash directory of the failed
+	test job is renamed to end with a '.stress-failed' suffix.
+
 You can also set the GIT_TEST_INSTALLED environment variable to
 the bindir of an existing git installation to test that installation.
 You still need to have built this git sandbox, from which various
@@ -425,7 +441,8 @@ This test harness library does the following things:
  - Creates an empty test directory with an empty .git/objects database
    and chdir(2) into it.  This directory is 't/trash
    directory.$test_name_without_dotsh', with t/ subject to change by
-   the --root option documented above.
+   the --root option documented above, and a '.stress-<N>' suffix
+   appended by the --stress option.
 
  - Defines standard test helper functions for your scripts to
    use.  These functions are designed to make all scripts behave
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 3746327bde..ee3435c6df 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1288,8 +1288,6 @@ test_set_port () {
 			# root-only port, use a larger one instead.
 			port=$(($port + 10000))
 		fi
-
-		eval $var=$port
 		;;
 	*[^0-9]*)
 		error >&7 "invalid port number: $port"
@@ -1298,4 +1296,9 @@ test_set_port () {
 		# The user has specified the port.
 		;;
 	esac
+
+	# Make sure that parallel '--stress' test jobs get different
+	# ports.
+	port=$(($port + ${GIT_TEST_STRESS_JOB_NR:-0}))
+	eval $var=$port
 }
diff --git a/t/test-lib.sh b/t/test-lib.sh
index d55d158580..e405191164 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -87,21 +87,110 @@ do
 		valgrind_only=${opt#--*=} ;;
 	--root=*)
 		root=${opt#--*=} ;;
+	--stress)
+		stress=t ;;
+	--stress=*)
+		stress=${opt#--*=} ;;
 	*)
 		# Other options will be handled later.
 	esac
 done
 
+TEST_STRESS_JOB_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
-TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME$TEST_STRESS_JOB_SFX"
+TRASH_DIRECTORY="trash directory.$TEST_NAME$TEST_STRESS_JOB_SFX"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
  *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
 esac
 
+# If --stress was passed, run this test repeatedly in several parallel loops.
+if test "$GIT_TEST_STRESS_STARTED" = "done"
+then
+	: # Don't stress test again.
+elif test -n "$stress"
+then
+	if test "$stress" != t
+	then
+		job_count=$stress
+	elif test -n "$GIT_TEST_STRESS_LOAD"
+	then
+		job_count="$GIT_TEST_STRESS_LOAD"
+	elif job_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null) &&
+	     test -n "$job_count"
+	then
+		job_count=$((2 * $job_count))
+	else
+		job_count=8
+	fi
+
+	mkdir -p "$TEST_RESULTS_DIR"
+	stressfail="$TEST_RESULTS_BASE.stress-failed"
+	rm -f "$stressfail"
+
+	stress_exit=0
+	trap '
+		kill $job_pids 2>/dev/null
+		wait
+		stress_exit=1
+	' TERM INT HUP
+
+	job_pids=
+	job_nr=0
+	while test $job_nr -lt "$job_count"
+	do
+		(
+			GIT_TEST_STRESS_STARTED=done
+			GIT_TEST_STRESS_JOB_NR=$job_nr
+			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
+
+			trap '
+				kill $test_pid 2>/dev/null
+				wait
+				exit 1
+			' TERM INT
+
+			cnt=0
+			while ! test -e "$stressfail"
+			do
+				$TEST_SHELL_PATH "$0" "$@" >"$TEST_RESULTS_BASE.stress-$job_nr.out" 2>&1 &
+				test_pid=$!
+
+				if wait $test_pid
+				then
+					printf "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				else
+					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
+					printf "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				fi
+				cnt=$(($cnt + 1))
+			done
+		) &
+		job_pids="$job_pids $!"
+		job_nr=$(($job_nr + 1))
+	done
+
+	wait
+
+	if test -f "$stressfail"
+	then
+		echo "Log(s) of failed test run(s):"
+		for failed_job_nr in $(sort -n "$stressfail")
+		do
+			echo "Contents of '$TEST_RESULTS_BASE.stress-$failed_job_nr.out':"
+			cat "$TEST_RESULTS_BASE.stress-$failed_job_nr.out"
+		done
+		rm -rf "$TRASH_DIRECTORY.stress-failed"
+		# Move the last one.
+		mv "$TRASH_DIRECTORY.stress-$failed_job_nr" "$TRASH_DIRECTORY.stress-failed"
+	fi
+
+	exit $stress_exit
+fi
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
@@ -340,7 +429,8 @@ do
 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
 	--valgrind=*|\
 	--valgrind-only=*|\
-	--root=*)
+	--root=*|\
+	--stress|--stress=*)
 		shift ;; # These options were handled already.
 	*)
 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
@@ -379,6 +469,13 @@ then
 	verbose=t
 fi
 
+if test -n "$stress"
+then
+	verbose=t
+	trace=t
+	immediate=t
+fi
+
 if test -n "$color"
 then
 	# Save the color control sequences now rather than run tput
-- 
2.20.0.rc2.156.g5a9fd2ce9c
SZEDER Gábor· Dec 10, 2018, 01:34 UTC · re: SZEDER Gábor · lore

[PATCH] fixup! test-lib: add the '--stress' option to run a test repeatedly under load

---

Erm. 'trace=t' must be set before checking whether the shell supports BASH_XTRACEFD.

 t/test-lib.sh | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)
Show changes to t/test-lib.sh +7 −7
diff --git a/t/test-lib.sh b/t/test-lib.sh
index e405191164..3e9916b39b 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -446,6 +446,13 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$stress"
+then
+	verbose=t
+	trace=t
+	immediate=t
+fi
+
 if test -n "$trace" && test -n "$test_untraceable"
 then
 	# '-x' tracing requested, but this test script can't be reliably
@@ -469,13 +476,6 @@ then
 	verbose=t
 fi
 
-if test -n "$stress"
-then
-	verbose=t
-	trace=t
-	immediate=t
-fi
-
 if test -n "$color"
 then
 	# Save the color control sequences now rather than run tput
-- 
2.20.0.rc2.156.g5a9fd2ce9c
Jeff King· Dec 11, 2018, 11:16 UTC · re: SZEDER Gábor · lore

Re: [PATCH v2 0/7] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

On Sun, Dec 09, 2018 at 11:56:21PM +0100, SZEDER Gábor wrote:
Show 12 quoted lines
> This patch series tries to make reproducing rare failures in flaky
> tests easier: it adds the '--stress' option to our test library to run
> the test script repeatedly in multiple parallel jobs, in the hope that
> the increased load creates enough variance in the timing of the test's
> commands that such a failure is eventually triggered.
> 
> Notable changes since v1:
> 
>   - Made it more Peff-friendly :), namely it will now show the log of
>     the failed test and will rename its trash directory.
> 
>     Furthermore, '--stress' will now imply '--verbose -x --immediate'.

:) Thanks. I do sympathize with the notion of keeping orthogonal things orthogonal, but I hope that this will make the tool a little more pleasant to use out of the box. We can always tweak the behavior later, too. It's not like this is something user-facing that we've promised as a scripting interface.

Show 5 quoted lines
>   - Improved abort handling based on the discussion of the previous
>     version.  (As a result, the patch is so heavily modified, that
>     'range-diff' with default parameters consideres it a different
>     patch; increasing the creation factor then results in one big ugly
>     diff of a diff, so I left it as-is.)
Yeah, this all looked good to me.
>   - Added a few new patches, mostly preparatory refactorings, though
>     the first one might be considered a bugfix.
I left a few minor comments, but these all looked good to me.
-Peff
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 0/8] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

To recap: this patch series tries to make reproducing rare failures in flaky tests easier: it adds the '--stress' option to our test library to run the test script repeatedly in multiple parallel jobs, in the hope that the increased load creates enough variance in the timing of the test's commands that such a failure is eventually triggered.

Changes since v2:
  - Most importantly, parse all options before handling '--tee',
    '--verbose-log' or '--stress'.  This constitutes the bulk of the
    range diff.
  - Add a bit of sanity check for the argument of '--stress=123'.
  - Minor commit message updates.

v2 was quickly followed up with a fixup! patch; the range-diff below includes that fixup! already squashed in.

Previous versions:
  v2: https://public-inbox.org/git/20181209225628.22216-1-szeder.dev@gmail.com/T/#u
  v1: https://public-inbox.org/git/20181204163457.15717-1-szeder.dev@gmail.com/T/
SZEDER Gábor (8):
  test-lib: translate SIGTERM and SIGHUP to an exit
  test-lib: parse options in a for loop to keep $@ intact
  test-lib: parse command line options earlier
  test-lib: consolidate naming of test-results paths
  test-lib: set $TRASH_DIRECTORY earlier
  test-lib: extract Bash version check for '-x' tracing
  test-lib-functions: introduce the 'test_set_port' helper function
  test-lib: add the '--stress' option to run a test repeatedly under
    load
 t/README                 |  19 ++-
 t/lib-git-daemon.sh      |   2 +-
 t/lib-git-p4.sh          |   9 +-
 t/lib-git-svn.sh         |   2 +-
 t/lib-httpd.sh           |   2 +-
 t/t0410-partial-clone.sh |   1 -
 t/t5512-ls-remote.sh     |   2 +-
 t/test-lib-functions.sh  |  39 +++++
 t/test-lib.sh            | 360 ++++++++++++++++++++++++++-------------
 9 files changed, 303 insertions(+), 133 deletions(-)
Range-diff:
1:  3a5c926167 = 1:  48a2b19218 test-lib: translate SIGTERM and SIGHUP to an exit
2:  8eee8d7fba < -:  ---------- test-lib: parse some --options earlier
-:  ---------- > 2:  adc5e8a86e test-lib: parse options in a for loop to keep $@ intact
-:  ---------- > 3:  89748074db test-lib: parse command line options earlier
3:  dd20ae5e9a ! 4:  5a6d17f54a test-lib: consolidate naming of test-results paths
    @@ -16,8 +16,8 @@
      --- a/t/test-lib.sh
      +++ b/t/test-lib.sh
     @@
    - 	esac
    - done
    + 	verbose=t
    + fi
      
     +TEST_NAME="$(basename "$0" .sh)"
     +TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
    @@ -27,8 +27,8 @@
      # additionally to the file test-results/$BASENAME.out, too.
      if test "$GIT_TEST_TEE_STARTED" = "done"
     @@
    - elif test -n "$tee" || test -n "$verbose_log" ||
    -      test -n "$valgrind" || test -n "$valgrind_only"
    + 	: # do not redirect again
    + elif test -n "$tee"
      then
     -	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
     -	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
4:  f6e56c91c4 ! 5:  cd7626b782 test-lib: set $TRASH_DIRECTORY earlier
    @@ -15,15 +15,6 @@
      diff --git a/t/test-lib.sh b/t/test-lib.sh
      --- a/t/test-lib.sh
      +++ b/t/test-lib.sh
    -@@
    - 		valgrind=${opt#--*=} ;;
    - 	--valgrind-only=*)
    - 		valgrind_only=${opt#--*=} ;;
    -+	--root=*)
    -+		root=${opt#--*=} ;;
    - 	*)
    - 		# Other options will be handled later.
    - 	esac
     @@
      TEST_NAME="$(basename "$0" .sh)"
      TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
    @@ -37,26 +28,6 @@
      
      # if --tee was passed, write the output not only to the terminal, but
      # additionally to the file test-results/$BASENAME.out, too.
    -@@
    - 		with_dashes=t; shift ;;
    - 	--no-color)
    - 		color=; shift ;;
    --	--root=*)
    --		root=${1#--*=}
    --		shift ;;
    - 	--chain-lint)
    - 		GIT_TEST_CHAIN_LINT=1
    - 		shift ;;
    -@@
    - 	-V|--verbose-log|\
    - 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
    - 	--valgrind=*|\
    --	--valgrind-only=*)
    -+	--valgrind-only=*|\
    -+	--root=*)
    - 		shift ;; # These options were handled already.
    - 	*)
    - 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
     @@
      fi
      
5:  99ecd2902e ! 6:  90296ac776 test-lib: extract Bash version check for '-x' tracing
    @@ -2,23 +2,30 @@
     
         test-lib: extract Bash version check for '-x' tracing
     
    -    Some of our test scripts can't be reliably run with '-x' tracing
    -    enabled unless executed with a Bash version supporting BASH_XTRACEFD
    -    (since v4.1), and we have a lengthy condition to disable tracing if
    -    such a test script is not executed with a suitable Bash version.
    +    One of our test scripts, 't1510-repo-setup.sh' [1], still can't be
    +    reliably run with '-x' tracing enabled, unless it's executed with a
    +    Bash version supporting BASH_XTRACEFD (since v4.1).  We have a lengthy
    +    condition to check the version of the shell running the test script,
    +    and disable tracing if it's not executed with a suitable Bash version
    +    [2].
     
         Move this check out from the option parsing loop, so other options can
         imply '-x' by setting 'trace=t', without missing this Bash version
         check.
     
    +    [1] 5827506928 (t1510-repo-setup: mark as untraceable with '-x',
    +        2018-02-24)
    +    [2] 5fc98e79fc (t: add means to disable '-x' tracing for individual
    +        test scripts, 2018-02-24)
    +
         Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
     
      diff --git a/t/test-lib.sh b/t/test-lib.sh
      --- a/t/test-lib.sh
      +++ b/t/test-lib.sh
     @@
    - 		GIT_TEST_CHAIN_LINT=0
    - 		shift ;;
    + 	--no-chain-lint)
    + 		GIT_TEST_CHAIN_LINT=0 ;;
      	-x)
     -		# Some test scripts can't be reliably traced  with '-x',
     -		# unless the test is run with a Bash version supporting
    @@ -38,10 +45,11 @@
     -		else
     -			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
     -		fi
    -+		trace=t
    - 		shift ;;
    - 	--tee|\
    - 	-V|--verbose-log|\
    +-		;;
    ++		trace=t ;;
    + 	-V|--verbose-log)
    + 		verbose_log=t
    + 		tee=t
     @@
      	test -z "$verbose_log" && verbose=t
      fi
6:  dcf7d2a397 ! 7:  a4a3e7cefa test-lib-functions: introduce the 'test_set_port' helper function
    @@ -178,7 +178,7 @@
     +
     +		eval $var=$port
     +		;;
    -+	*[^0-9]*)
    ++	*[^0-9]*|0*)
     +		error >&7 "invalid port number: $port"
     +		;;
     +	*)
7:  f27f2fc1d0 ! 8:  b0dcff0d3f test-lib: add the '--stress' option to run a test repeatedly under load
    @@ -4,12 +4,12 @@
     
         Unfortunately, we have a few flaky tests, whose failures tend to be
         hard to reproduce.  We've found that the best we can do to reproduce
    -    such a failure is to run the test repeatedly while the machine is
    -    under load, and wait in the hope that the load creates enough variance
    -    in the timing of the test's commands that a failure is evenually
    -    triggered.  I have a command to do that, and I noticed that two other
    -    contributors have rolled their own scripts to do the same, all
    -    choosing slightly different approaches.
    +    such a failure is to run the test script repeatedly while the machine
    +    is under load, and wait in the hope that the load creates enough
    +    variance in the timing of the test's commands that a failure is
    +    evenually triggered.  I have a command to do that, and I noticed that
    +    two other contributors have rolled their own scripts to do the same,
    +    all choosing slightly different approaches.
     
         To help reproduce failures in flaky tests, introduce the '--stress'
         option to run a test script repeatedly in multiple parallel jobs until
    @@ -37,19 +37,21 @@
             user or to the test number, so even tests involving daemons
             listening on a TCP socket can be stressed.
     
    -      - Redirect each parallel test run's output to
    +      - Redirect each parallel test run's verbose output to
             't/test-results/$TEST_NAME.stress-<nr>.out', because dumping the
             output of several parallel running tests to the terminal would
             create a big ugly mess.
     
         For convenience, print the output of the failed test job at the end,
         and rename its trash directory to end with the '.stress-failed'
    -    suffix, so it's easy to find in a predictable path.  If, in an
    -    unlikely case, more than one jobs were to fail nearly at the same
    -    time, then print the output of all failed jobs, and rename the trash
    -    directory of only the last one (i.e. with the highest job number), as
    -    it is the trash directory of the test whose output will be at the
    -    bottom of the user's terminal.
    +    suffix, so it's easy to find in a predictable path (OTOH, all absolute
    +    paths recorded in the trash directory become invalid; we'll see
    +    whether this causes any issues in practice).  If, in an unlikely case,
    +    more than one jobs were to fail nearly at the same time, then print
    +    the output of all failed jobs, and rename the trash directory of only
    +    the last one (i.e. with the highest job number), as it is the trash
    +    directory of the test whose output will be at the bottom of the user's
    +    terminal.
     
         Based on Jeff King's 'stress' script.
     
    @@ -102,7 +104,7 @@
     -
     -		eval $var=$port
      		;;
    - 	*[^0-9]*)
    + 	*[^0-9]*|0*)
      		error >&7 "invalid port number: $port"
     @@
      		# The user has specified the port.
    @@ -119,17 +121,42 @@
      --- a/t/test-lib.sh
      +++ b/t/test-lib.sh
     @@
    - 		valgrind_only=${opt#--*=} ;;
    - 	--root=*)
    - 		root=${opt#--*=} ;;
    + 		verbose_log=t
    + 		tee=t
    + 		;;
     +	--stress)
     +		stress=t ;;
     +	--stress=*)
    -+		stress=${opt#--*=} ;;
    ++		stress=${opt#--*=}
    ++		case "$stress" in
    ++		*[^0-9]*|0*|"")
    ++			echo "error: --stress=<N> requires the number of jobs to run" >&2
    ++			exit 1
    ++			;;
    ++		*)	# Good.
    ++			;;
    ++		esac
    ++		;;
      	*)
    - 		# Other options will be handled later.
    + 		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
      	esac
    - done
    +@@
    + 	test -z "$verbose_log" && verbose=t
    + fi
    + 
    ++if test -n "$stress"
    ++then
    ++	verbose=t
    ++	trace=t
    ++	immediate=t
    ++fi
    ++
    + if test -n "$trace" && test -n "$test_untraceable"
    + then
    + 	# '-x' tracing requested, but this test script can't be reliably
    +@@
    + 	verbose=t
    + fi
      
     +TEST_STRESS_JOB_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
      TEST_NAME="$(basename "$0" .sh)"
    @@ -231,27 +258,3 @@
      # if --tee was passed, write the output not only to the terminal, but
      # additionally to the file test-results/$BASENAME.out, too.
      if test "$GIT_TEST_TEE_STARTED" = "done"
    -@@
    - 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind|\
    - 	--valgrind=*|\
    - 	--valgrind-only=*|\
    --	--root=*)
    -+	--root=*|\
    -+	--stress|--stress=*)
    - 		shift ;; # These options were handled already.
    - 	*)
    - 		echo "error: unknown test option '$1'" >&2; exit 1 ;;
    -@@
    - 	verbose=t
    - fi
    - 
    -+if test -n "$stress"
    -+then
    -+	verbose=t
    -+	trace=t
    -+	immediate=t
    -+fi
    -+
    - if test -n "$color"
    - then
    - 	# Save the color control sequences now rather than run tput
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 1/8] test-lib: translate SIGTERM and SIGHUP to an exit

Right now if a test script receives SIGTERM or SIGHUP (e.g., because a test was hanging and the user 'kill'-ed it or simply closed the terminal window the test was running in), the shell exits immediately. This can be annoying if the test script did any global setup, like starting apache or git-daemon, as it will not have an opportunity to clean up after itself. A subsequent run of the test won't be able to start its own daemon, and will either fail or skip the tests.

Instead, let's trap SIGTERM and SIGHUP as well to make sure we do a clean shutdown, and just chain it to a normal exit (which will trigger any cleanup).

This patch follows suit of da706545f7 (t: translate SIGINT to an exit, 2015-03-13), and even stole its commit message as well.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to t/test-lib.sh +1 −1
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 0f1faa24b2..9a3f7930a3 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -476,7 +476,7 @@ die () {
 
 GIT_EXIT_OK=
 trap 'die' EXIT
-trap 'exit $?' INT
+trap 'exit $?' INT TERM HUP
 
 # The user-facing functions are loaded from a separate file so that
 # test_perf subshells can have them too
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 2/8] test-lib: parse options in a for loop to keep $@ intact

'test-lib.sh' looks for the presence of certain options like '--tee' and '--verbose-log', so it can execute the test script again to save its standard output and error, and to do so it needs the original command line options the test was invoked with.

The next patch is about to move the option parsing loop earlier in 'test-lib.sh', but it is implemented using 'shift' in a while loop, effecively destroying "$@" by the end of the option parsing. Not good.

As a preparatory step, turn that option parsing loop into a 'for opt in "$@"' loop to preserve "$@" intact while iterating over the options, and taking extra care to handle the '-r' option's required argument (or the lack thereof).

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 77 ++++++++++++++++++++++++++++-----------------------
 1 file changed, 42 insertions(+), 35 deletions(-)
Show changes to t/test-lib.sh +42 −35
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 9a3f7930a3..ed7267962f 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -264,58 +264,59 @@ test "x$TERM" != "xdumb" && (
 	) &&
 	color=t
 
-while test "$#" -ne 0
+store_arg_to=
+prev_opt=
+for opt
 do
-	case "$1" in
+	if test -n "$store_arg_to"
+	then
+		eval $store_arg_to=\$opt
+		store_arg_to=
+		prev_opt=
+		continue
+	fi
+
+	case "$opt" in
 	-d|--d|--de|--deb|--debu|--debug)
-		debug=t; shift ;;
+		debug=t ;;
 	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
-		immediate=t; shift ;;
+		immediate=t ;;
 	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG; shift ;;
+		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
 	-r)
-		shift; test "$#" -ne 0 || {
-			echo 'error: -r requires an argument' >&2;
-			exit 1;
-		}
-		run_list=$1; shift ;;
+		store_arg_to=run_list
+		;;
 	--run=*)
-		run_list=${1#--*=}; shift ;;
+		run_list=${opt#--*=} ;;
 	-h|--h|--he|--hel|--help)
-		help=t; shift ;;
+		help=t ;;
 	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
-		verbose=t; shift ;;
+		verbose=t ;;
 	--verbose-only=*)
-		verbose_only=${1#--*=}
-		shift ;;
+		verbose_only=${opt#--*=}
+		;;
 	-q|--q|--qu|--qui|--quie|--quiet)
 		# Ignore --quiet under a TAP::Harness. Saying how many tests
 		# passed without the ok/not ok details is always an error.
-		test -z "$HARNESS_ACTIVE" && quiet=t; shift ;;
+		test -z "$HARNESS_ACTIVE" && quiet=t ;;
 	--with-dashes)
-		with_dashes=t; shift ;;
+		with_dashes=t ;;
 	--no-color)
-		color=; shift ;;
+		color= ;;
 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
-		valgrind=memcheck
-		shift ;;
+		valgrind=memcheck ;;
 	--valgrind=*)
-		valgrind=${1#--*=}
-		shift ;;
+		valgrind=${opt#--*=} ;;
 	--valgrind-only=*)
-		valgrind_only=${1#--*=}
-		shift ;;
+		valgrind_only=${opt#--*=} ;;
 	--tee)
-		shift ;; # was handled already
+		;; # was handled already
 	--root=*)
-		root=${1#--*=}
-		shift ;;
+		root=${opt#--*=} ;;
 	--chain-lint)
-		GIT_TEST_CHAIN_LINT=1
-		shift ;;
+		GIT_TEST_CHAIN_LINT=1 ;;
 	--no-chain-lint)
-		GIT_TEST_CHAIN_LINT=0
-		shift ;;
+		GIT_TEST_CHAIN_LINT=0 ;;
 	-x)
 		# Some test scripts can't be reliably traced  with '-x',
 		# unless the test is run with a Bash version supporting
@@ -335,14 +336,20 @@ do
 		else
 			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
 		fi
-		shift ;;
+		;;
 	-V|--verbose-log)
-		verbose_log=t
-		shift ;;
+		verbose_log=t ;;
 	*)
-		echo "error: unknown test option '$1'" >&2; exit 1 ;;
+		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
 	esac
+
+	prev_opt=$opt
 done
+if test -n "$store_arg_to"
+then
+	echo "error: $prev_opt requires an argument" >&2
+	exit 1
+fi
 
 if test -n "$valgrind_only"
 then
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 3/8] test-lib: parse command line options earlier

'test-lib.sh' looks for the presence of certain options like '--tee' and '--verbose-log', so it can execute the test script again to save its standard output and error. It looks for '--valgrind' as well, to set up some Valgrind-specific stuff. These all happen before the actual option parsing loop, and the conditions looking for these options look a bit odd, too. They are not completely correct, either, because in a bogus invocation like './t1234-foo.sh -r --tee' they recognize '--tee', although it should be handled as the required argument of the '-r' option. This patch series will add two more options to look out for early, and, in addition, will have to extract these options' stuck arguments (i.e. '--opt=arg') as well.

So let's move the option parsing loop and the couple of related conditions following it earlier in 'test-lib.sh', before the place where the test script is executed again for '--tee' and its friends.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 228 ++++++++++++++++++++++++++------------------------
 1 file changed, 119 insertions(+), 109 deletions(-)
Show changes to t/test-lib.sh +119 −109
diff --git a/t/test-lib.sh b/t/test-lib.sh
index ed7267962f..fcc04afdff 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -71,13 +71,125 @@ then
 	exit 1
 fi
 
+# Parse options while taking care to leave $@ intact, so we will still
+# have all the original command line options when executing the test
+# script again for '--tee' and '--verbose-log' below.
+store_arg_to=
+prev_opt=
+for opt
+do
+	if test -n "$store_arg_to"
+	then
+		eval $store_arg_to=\$opt
+		store_arg_to=
+		prev_opt=
+		continue
+	fi
+
+	case "$opt" in
+	-d|--d|--de|--deb|--debu|--debug)
+		debug=t ;;
+	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
+		immediate=t ;;
+	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
+		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
+	-r)
+		store_arg_to=run_list
+		;;
+	--run=*)
+		run_list=${opt#--*=} ;;
+	-h|--h|--he|--hel|--help)
+		help=t ;;
+	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
+		verbose=t ;;
+	--verbose-only=*)
+		verbose_only=${opt#--*=}
+		;;
+	-q|--q|--qu|--qui|--quie|--quiet)
+		# Ignore --quiet under a TAP::Harness. Saying how many tests
+		# passed without the ok/not ok details is always an error.
+		test -z "$HARNESS_ACTIVE" && quiet=t ;;
+	--with-dashes)
+		with_dashes=t ;;
+	--no-color)
+		color= ;;
+	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
+		valgrind=memcheck
+		tee=t
+		;;
+	--valgrind=*)
+		valgrind=${opt#--*=}
+		tee=t
+		;;
+	--valgrind-only=*)
+		valgrind_only=${opt#--*=}
+		tee=t
+		;;
+	--tee)
+		tee=t ;;
+	--root=*)
+		root=${opt#--*=} ;;
+	--chain-lint)
+		GIT_TEST_CHAIN_LINT=1 ;;
+	--no-chain-lint)
+		GIT_TEST_CHAIN_LINT=0 ;;
+	-x)
+		# Some test scripts can't be reliably traced  with '-x',
+		# unless the test is run with a Bash version supporting
+		# BASH_XTRACEFD (introduced in Bash v4.1).  Check whether
+		# this test is marked as such, and ignore '-x' if it
+		# isn't executed with a suitable Bash version.
+		if test -z "$test_untraceable" || {
+		     test -n "$BASH_VERSION" && {
+		       test ${BASH_VERSINFO[0]} -gt 4 || {
+			 test ${BASH_VERSINFO[0]} -eq 4 &&
+			 test ${BASH_VERSINFO[1]} -ge 1
+		       }
+		     }
+		   }
+		then
+			trace=t
+		else
+			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		fi
+		;;
+	-V|--verbose-log)
+		verbose_log=t
+		tee=t
+		;;
+	*)
+		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
+	esac
+
+	prev_opt=$opt
+done
+if test -n "$store_arg_to"
+then
+	echo "error: $prev_opt requires an argument" >&2
+	exit 1
+fi
+
+if test -n "$valgrind_only"
+then
+	test -z "$valgrind" && valgrind=memcheck
+	test -z "$verbose" && verbose_only="$valgrind_only"
+elif test -n "$valgrind"
+then
+	test -z "$verbose_log" && verbose=t
+fi
+
+if test -n "$trace" && test -z "$verbose_log"
+then
+	verbose=t
+fi
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
-case "$GIT_TEST_TEE_STARTED, $* " in
-done,*)
-	# do not redirect again
-	;;
-*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
+if test "$GIT_TEST_TEE_STARTED" = "done"
+then
+	: # do not redirect again
+elif test -n "$tee"
+then
 	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
 	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
 
@@ -94,8 +206,7 @@ done,*)
 	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
 	test "$(cat "$BASE.exit")" = 0
 	exit
-	;;
-esac
+fi
 
 # For repeatability, reset the environment to known value.
 # TERM is sanitized below, after saving color control sequences.
@@ -193,7 +304,7 @@ fi
 
 # Add libc MALLOC and MALLOC_PERTURB test
 # only if we are not executing the test with valgrind
-if expr " $GIT_TEST_OPTS " : ".* --valgrind " >/dev/null ||
+if test -n "$valgrind" ||
    test -n "$TEST_NO_MALLOC_CHECK"
 then
 	setup_malloc_check () {
@@ -264,107 +375,6 @@ test "x$TERM" != "xdumb" && (
 	) &&
 	color=t
 
-store_arg_to=
-prev_opt=
-for opt
-do
-	if test -n "$store_arg_to"
-	then
-		eval $store_arg_to=\$opt
-		store_arg_to=
-		prev_opt=
-		continue
-	fi
-
-	case "$opt" in
-	-d|--d|--de|--deb|--debu|--debug)
-		debug=t ;;
-	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
-		immediate=t ;;
-	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
-	-r)
-		store_arg_to=run_list
-		;;
-	--run=*)
-		run_list=${opt#--*=} ;;
-	-h|--h|--he|--hel|--help)
-		help=t ;;
-	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
-		verbose=t ;;
-	--verbose-only=*)
-		verbose_only=${opt#--*=}
-		;;
-	-q|--q|--qu|--qui|--quie|--quiet)
-		# Ignore --quiet under a TAP::Harness. Saying how many tests
-		# passed without the ok/not ok details is always an error.
-		test -z "$HARNESS_ACTIVE" && quiet=t ;;
-	--with-dashes)
-		with_dashes=t ;;
-	--no-color)
-		color= ;;
-	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
-		valgrind=memcheck ;;
-	--valgrind=*)
-		valgrind=${opt#--*=} ;;
-	--valgrind-only=*)
-		valgrind_only=${opt#--*=} ;;
-	--tee)
-		;; # was handled already
-	--root=*)
-		root=${opt#--*=} ;;
-	--chain-lint)
-		GIT_TEST_CHAIN_LINT=1 ;;
-	--no-chain-lint)
-		GIT_TEST_CHAIN_LINT=0 ;;
-	-x)
-		# Some test scripts can't be reliably traced  with '-x',
-		# unless the test is run with a Bash version supporting
-		# BASH_XTRACEFD (introduced in Bash v4.1).  Check whether
-		# this test is marked as such, and ignore '-x' if it
-		# isn't executed with a suitable Bash version.
-		if test -z "$test_untraceable" || {
-		     test -n "$BASH_VERSION" && {
-		       test ${BASH_VERSINFO[0]} -gt 4 || {
-			 test ${BASH_VERSINFO[0]} -eq 4 &&
-			 test ${BASH_VERSINFO[1]} -ge 1
-		       }
-		     }
-		   }
-		then
-			trace=t
-		else
-			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		fi
-		;;
-	-V|--verbose-log)
-		verbose_log=t ;;
-	*)
-		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
-	esac
-
-	prev_opt=$opt
-done
-if test -n "$store_arg_to"
-then
-	echo "error: $prev_opt requires an argument" >&2
-	exit 1
-fi
-
-if test -n "$valgrind_only"
-then
-	test -z "$valgrind" && valgrind=memcheck
-	test -z "$verbose" && verbose_only="$valgrind_only"
-elif test -n "$valgrind"
-then
-	test -z "$verbose_log" && verbose=t
-fi
-
-if test -n "$trace" && test -z "$verbose_log"
-then
-	verbose=t
-fi
-
 if test -n "$color"
 then
 	# Save the color control sequences now rather than run tput
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 6/8] test-lib: extract Bash version check for '-x' tracing

One of our test scripts, 't1510-repo-setup.sh' [1], still can't be reliably run with '-x' tracing enabled, unless it's executed with a Bash version supporting BASH_XTRACEFD (since v4.1). We have a lengthy condition to check the version of the shell running the test script, and disable tracing if it's not executed with a suitable Bash version [2].

Move this check out from the option parsing loop, so other options can imply '-x' by setting 'trace=t', without missing this Bash version check.

[1] 5827506928 (t1510-repo-setup: mark as untraceable with '-x',
    2018-02-24)
[2] 5fc98e79fc (t: add means to disable '-x' tracing for individual
    test scripts, 2018-02-24)
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 38 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 19 deletions(-)
Show changes to t/test-lib.sh +19 −19
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 2b88ba2de1..7d77a26d44 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -134,25 +134,7 @@ do
 	--no-chain-lint)
 		GIT_TEST_CHAIN_LINT=0 ;;
 	-x)
-		# Some test scripts can't be reliably traced  with '-x',
-		# unless the test is run with a Bash version supporting
-		# BASH_XTRACEFD (introduced in Bash v4.1).  Check whether
-		# this test is marked as such, and ignore '-x' if it
-		# isn't executed with a suitable Bash version.
-		if test -z "$test_untraceable" || {
-		     test -n "$BASH_VERSION" && {
-		       test ${BASH_VERSINFO[0]} -gt 4 || {
-			 test ${BASH_VERSINFO[0]} -eq 4 &&
-			 test ${BASH_VERSINFO[1]} -ge 1
-		       }
-		     }
-		   }
-		then
-			trace=t
-		else
-			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		fi
-		;;
+		trace=t ;;
 	-V|--verbose-log)
 		verbose_log=t
 		tee=t
@@ -178,6 +160,24 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$trace" && test -n "$test_untraceable"
+then
+	# '-x' tracing requested, but this test script can't be reliably
+	# traced, unless it is run with a Bash version supporting
+	# BASH_XTRACEFD (introduced in Bash v4.1).
+	if test -n "$BASH_VERSION" && {
+	     test ${BASH_VERSINFO[0]} -gt 4 || {
+	       test ${BASH_VERSINFO[0]} -eq 4 &&
+	       test ${BASH_VERSINFO[1]} -ge 1
+	     }
+	   }
+	then
+		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
+	else
+		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		trace=
+	fi
+fi
 if test -n "$trace" && test -z "$verbose_log"
 then
 	verbose=t
-- 
2.20.1.151.gec613c4b75
Carlo Arenas· Dec 31, 2018, 17:14 UTC · re: SZEDER Gábor · lore

Re: [PATCH v3 6/8] test-lib: extract Bash version check for '-x' tracing

slightly offtopic and definitely not meant to be relevant as a critic on this specific change which I found otherwise fine, but just a heads up that this test itself is bash specific (since it uses arrays, against the recommendation in Documentation/CodingGuidelines) and therefore breaks, at least in NetBSD as reported in :

  https://public-inbox.org/git/20181121224929.GD5348@jessie.local/
Carlo
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 7/8] test-lib-functions: introduce the 'test_set_port' helper function

Several test scripts run daemons like 'git-daemon' or Apache, and communicate with them through TCP sockets. To have unique ports where these daemons are accessible, the ports are usually the number of the corresponding test scripts, unless the user overrides them via environment variables, and thus all those tests and test libs contain more or less the same bit of one-liner boilerplate code to find out the port. The last patch in this series will make this a bit more complicated.

Factor out finding the port for a daemon into the common helper function 'test_set_port' to avoid repeating ourselves.

Take special care of test scripts with "low" numbers:
  - Test numbers below 1024 would result in a port that's only usable
    as root, so set their port to '10000 + test-nr' to make sure it
    doesn't interfere with other tests in the test suite.  This makes
    the hardcoded port number in 't0410-partial-clone.sh' unnecessary,
    remove it.
  - The shell's arithmetic evaluation interprets numbers with leading
    zeros as octal values, which means that test number below 1000 and
    containing the digits 8 or 9 will trigger an error.  Remove all
    leading zeros from the test numbers to prevent this.

Note that the 'git p4' tests are unlike the other tests involving daemons in that:

  - 'lib-git-p4.sh' doesn't use the test's number for unique port as
    is, but does a bit of additional arithmetic on top [1].
  - The port is not overridable via an environment variable.

With this patch even 'git p4' tests will use the test's number as default port, and it will be overridable via the P4DPORT environment variable.

[1] Commit fc00233071 (git-p4 tests: refactor and cleanup, 2011-08-22)
    introduced that "unusual" unique port computation without
    explaining why it was necessary (as opposed to simply using the
    test number as is).  It seems to be just unnecessary complication,
    and in any case that commit came way before the "test nr as unique
    port" got "standardized" for other daemons in commits c44132fcf3
    (tests: auto-set git-daemon port, 2014-02-10), 3bb486e439 (tests:
    auto-set LIB_HTTPD_PORT from test name, 2014-02-10), and
    bf9d7df950 (t/lib-git-svn.sh: improve svnserve tests with parallel
    make test, 2017-12-01).
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/lib-git-daemon.sh      |  2 +-
 t/lib-git-p4.sh          |  9 +--------
 t/lib-git-svn.sh         |  2 +-
 t/lib-httpd.sh           |  2 +-
 t/t0410-partial-clone.sh |  1 -
 t/t5512-ls-remote.sh     |  2 +-
 t/test-lib-functions.sh  | 36 ++++++++++++++++++++++++++++++++++++
 7 files changed, 41 insertions(+), 13 deletions(-)
Show changes to 7 files +41 −13

t/lib-git-daemon.sh, t/lib-git-p4.sh, t/lib-git-svn.sh, t/lib-httpd.sh, t/t0410-partial-clone.sh, t/t5512-ls-remote.sh, t/test-lib-functions.sh

diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh
index f98de95c15..41eb1e3ae8 100644
--- a/t/lib-git-daemon.sh
+++ b/t/lib-git-daemon.sh
@@ -28,7 +28,7 @@ then
 	test_skip_or_die $GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
 fi
 
-LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-${this_test#t}}
+test_set_port LIB_GIT_DAEMON_PORT
 
 GIT_DAEMON_PID=
 GIT_DAEMON_DOCUMENT_ROOT_PATH="$PWD"/repo
diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh
index c27599474c..b3be3ba011 100644
--- a/t/lib-git-p4.sh
+++ b/t/lib-git-p4.sh
@@ -53,14 +53,7 @@ time_in_seconds () {
 	(cd / && "$PYTHON_PATH" -c 'import time; print(int(time.time()))')
 }
 
-# Try to pick a unique port: guess a large number, then hope
-# no more than one of each test is running.
-#
-# This does not handle the case where somebody else is running the
-# same tests and has chosen the same ports.
-testid=${this_test#t}
-git_p4_test_start=9800
-P4DPORT=$((10669 + ($testid - $git_p4_test_start)))
+test_set_port P4DPORT
 
 P4PORT=localhost:$P4DPORT
 P4CLIENT=client
diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
index a8130f9119..f3b478c307 100644
--- a/t/lib-git-svn.sh
+++ b/t/lib-git-svn.sh
@@ -13,6 +13,7 @@ fi
 GIT_DIR=$PWD/.git
 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
@@ -119,7 +120,6 @@ require_svnserve () {
 }
 
 start_svnserve () {
-	SVNSERVE_PORT=${SVNSERVE_PORT-${this_test#t}}
 	svnserve --listen-port $SVNSERVE_PORT \
 		 --root "$rawsvnrepo" \
 		 --listen-once \
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index a8729f8232..e465116ef9 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -82,7 +82,7 @@ case $(uname) in
 esac
 
 LIB_HTTPD_PATH=${LIB_HTTPD_PATH-"$DEFAULT_HTTPD_PATH"}
-LIB_HTTPD_PORT=${LIB_HTTPD_PORT-${this_test#t}}
+test_set_port LIB_HTTPD_PORT
 
 TEST_PATH="$TEST_DIRECTORY"/lib-httpd
 HTTPD_ROOT_PATH="$PWD"/httpd
diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh
index ba3887f178..0aca8d7588 100755
--- a/t/t0410-partial-clone.sh
+++ b/t/t0410-partial-clone.sh
@@ -480,7 +480,6 @@ test_expect_success 'gc stops traversal when a missing but promised object is re
 	! grep "$TREE_HASH" out
 '
 
-LIB_HTTPD_PORT=12345  # default port, 410, cannot be used as non-root
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 32e722db2e..cd9e60632d 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -260,7 +260,7 @@ test_lazy_prereq GIT_DAEMON '
 # This test spawns a daemon, so run it only if the user would be OK with
 # testing with git-daemon.
 test_expect_success PIPE,JGIT,GIT_DAEMON 'indicate no refs in standards-compliant empty remote' '
-	JGIT_DAEMON_PORT=${JGIT_DAEMON_PORT-${this_test#t}} &&
+	test_set_port JGIT_DAEMON_PORT &&
 	JGIT_DAEMON_PID= &&
 	git init --bare empty.git &&
 	>empty.git/git-daemon-export-ok &&
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 6b3bbf99e4..4459bdda13 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1263,3 +1263,39 @@ test_oid () {
 	fi &&
 	eval "printf '%s' \"\${$var}\""
 }
+
+# Choose a port number based on the test script's number and store it in
+# the given variable name, unless that variable already contains a number.
+test_set_port () {
+	local var=$1 port
+
+	if test $# -ne 1 || test -z "$var"
+	then
+		BUG "test_set_port requires a variable name"
+	fi
+
+	eval port=\$$var
+	case "$port" in
+	"")
+		# No port is set in the given env var, use the test
+		# number as port number instead.
+		# Remove not only the leading 't', but all leading zeros
+		# as well, so the arithmetic below won't (mis)interpret
+		# a test number like '0123' as an octal value.
+		port=${this_test#${this_test%%[1-9]*}}
+		if test "${port:-0}" -lt 1024
+		then
+			# root-only port, use a larger one instead.
+			port=$(($port + 10000))
+		fi
+
+		eval $var=$port
+		;;
+	*[^0-9]*|0*)
+		error >&7 "invalid port number: $port"
+		;;
+	*)
+		# The user has specified the port.
+		;;
+	esac
+}
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 5/8] test-lib: set $TRASH_DIRECTORY earlier

A later patch in this series will need to know the path to the trash directory early in 'test-lib.sh', but $TRASH_DIRECTORY is set much later. Furthermore, the path to the trash directory depends on the '--root=<path>' option, which, too, is parsed too late.

Move parsing '--root=...' to the early option parsing loop, and set $TRASH_DIRECTORY where the other test-specific path variables are set.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to t/test-lib.sh +6 −6
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 41457d1dcf..2b88ba2de1 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -186,6 +186,12 @@ fi
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
 TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
+test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
+case "$TRASH_DIRECTORY" in
+/*) ;; # absolute path is good
+ *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
+esac
 
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
@@ -1046,12 +1052,6 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
-test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
-case "$TRASH_DIRECTORY" in
-/*) ;; # absolute path is good
- *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
-esac
 rm -fr "$TRASH_DIRECTORY" || {
 	GIT_EXIT_OK=t
 	echo >&5 "FATAL: Cannot prepare test area"
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 22:44 UTC · re: SZEDER Gábor · lore

Re: [PATCH v3 5/8] test-lib: set $TRASH_DIRECTORY earlier

On Sun, Dec 30, 2018 at 08:16:26PM +0100, SZEDER Gábor wrote:
Show 7 quoted lines
> A later patch in this series will need to know the path to the trash
> directory early in 'test-lib.sh', but $TRASH_DIRECTORY is set much
> later.  Furthermore, the path to the trash directory depends on the
> '--root=<path>' option, which, too, is parsed too late.
> 
> Move parsing '--root=...' to the early option parsing loop, and set
> $TRASH_DIRECTORY where the other test-specific path variables are set.

Sigh. After moving the whole option parsing loop, I should have updated this commit message as well.

Hang on for a sec...
Show 38 quoted lines
> Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
> ---
>  t/test-lib.sh | 12 ++++++------
>  1 file changed, 6 insertions(+), 6 deletions(-)
> 
> diff --git a/t/test-lib.sh b/t/test-lib.sh
> index 41457d1dcf..2b88ba2de1 100644
> --- a/t/test-lib.sh
> +++ b/t/test-lib.sh
> @@ -186,6 +186,12 @@ fi
>  TEST_NAME="$(basename "$0" .sh)"
>  TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
>  TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
> +TRASH_DIRECTORY="trash directory.$TEST_NAME"
> +test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
> +case "$TRASH_DIRECTORY" in
> +/*) ;; # absolute path is good
> + *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
> +esac
>  
>  # if --tee was passed, write the output not only to the terminal, but
>  # additionally to the file test-results/$BASENAME.out, too.
> @@ -1046,12 +1052,6 @@ then
>  fi
>  
>  # Test repository
> -TRASH_DIRECTORY="trash directory.$TEST_NAME"
> -test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
> -case "$TRASH_DIRECTORY" in
> -/*) ;; # absolute path is good
> - *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
> -esac
>  rm -fr "$TRASH_DIRECTORY" || {
>  	GIT_EXIT_OK=t
>  	echo >&5 "FATAL: Cannot prepare test area"
> -- 
> 2.20.1.151.gec613c4b75
> 
SZEDER Gábor· Dec 30, 2018, 22:48 UTC · re: SZEDER Gábor · lore

[PATCH v3.1 5/8] test-lib: set $TRASH_DIRECTORY earlier

A later patch in this series will need to know the path to the trash directory early in 'test-lib.sh', but $TRASH_DIRECTORY is set much later.

Set $TRASH_DIRECTORY earlier, where the other test-specific path variables are set.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
Updated the commit message, the diff is the same.
 t/test-lib.sh | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to t/test-lib.sh +6 −6
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 41457d1dcf..2b88ba2de1 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -186,6 +186,12 @@ fi
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
 TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
+test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
+case "$TRASH_DIRECTORY" in
+/*) ;; # absolute path is good
+ *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
+esac
 
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
@@ -1046,12 +1052,6 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
-test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
-case "$TRASH_DIRECTORY" in
-/*) ;; # absolute path is good
- *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
-esac
 rm -fr "$TRASH_DIRECTORY" || {
 	GIT_EXIT_OK=t
 	echo >&5 "FATAL: Cannot prepare test area"
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 4/8] test-lib: consolidate naming of test-results paths

There are two places where we strip off any leading path components and the '.sh' suffix from the test script's pathname, and there are four places where we construct the name of the 't/test-results' directory or the name of various test-specific files in there. The last patch in this series will add even more.

Factor these out into helper variables to avoid repeating ourselves.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
Show changes to t/test-lib.sh +11 −11
diff --git a/t/test-lib.sh b/t/test-lib.sh
index fcc04afdff..41457d1dcf 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -183,6 +183,10 @@ then
 	verbose=t
 fi
 
+TEST_NAME="$(basename "$0" .sh)"
+TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
@@ -190,12 +194,11 @@ then
 	: # do not redirect again
 elif test -n "$tee"
 then
-	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
-	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
+	mkdir -p "$TEST_RESULTS_DIR"
 
 	# Make this filename available to the sub-process in case it is using
 	# --verbose-log.
-	GIT_TEST_TEE_OUTPUT_FILE=$BASE.out
+	GIT_TEST_TEE_OUTPUT_FILE=$TEST_RESULTS_BASE.out
 	export GIT_TEST_TEE_OUTPUT_FILE
 
 	# Truncate before calling "tee -a" to get rid of the results
@@ -203,8 +206,8 @@ then
 	>"$GIT_TEST_TEE_OUTPUT_FILE"
 
 	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
-	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
-	test "$(cat "$BASE.exit")" = 0
+	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
+	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
 	exit
 fi
 
@@ -835,12 +838,9 @@ test_done () {
 
 	if test -z "$HARNESS_ACTIVE"
 	then
-		test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results"
-		mkdir -p "$test_results_dir"
-		base=${0##*/}
-		test_results_path="$test_results_dir/${base%.sh}.counts"
+		mkdir -p "$TEST_RESULTS_DIR"
 
-		cat >"$test_results_path" <<-EOF
+		cat >"$TEST_RESULTS_BASE.counts" <<-EOF
 		total $test_count
 		success $test_success
 		fixed $test_fixed
@@ -1046,7 +1046,7 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$(basename "$0" .sh)"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Dec 30, 2018, 19:16 UTC · re: SZEDER Gábor · lore

[PATCH v3 8/8] test-lib: add the '--stress' option to run a test repeatedly under load

Unfortunately, we have a few flaky tests, whose failures tend to be hard to reproduce. We've found that the best we can do to reproduce such a failure is to run the test script repeatedly while the machine is under load, and wait in the hope that the load creates enough variance in the timing of the test's commands that a failure is evenually triggered. I have a command to do that, and I noticed that two other contributors have rolled their own scripts to do the same, all choosing slightly different approaches.

To help reproduce failures in flaky tests, introduce the '--stress' option to run a test script repeatedly in multiple parallel jobs until one of them fails, thereby using the test script itself to increase the load on the machine.

The number of parallel jobs is determined by, in order of precedence: the number specified as '--stress=<N>', or the value of the GIT_TEST_STRESS_LOAD environment variable, or twice the number of available processors (as reported by the 'getconf' utility), or 8.

Make '--stress' imply '--verbose -x --immediate' to get the most information about rare failures; there is really no point in spending all the extra effort to reproduce such a failure, and then not know which command failed and why.

To prevent the several parallel invocations of the same test from interfering with each other:

  - Include the parallel job's number in the name of the trash
    directory and the various output files under 't/test-results/' as
    a '.stress-<Nr>' suffix.
  - Add the parallel job's number to the port number specified by the
    user or to the test number, so even tests involving daemons
    listening on a TCP socket can be stressed.
  - Redirect each parallel test run's verbose output to
    't/test-results/$TEST_NAME.stress-<nr>.out', because dumping the
    output of several parallel running tests to the terminal would
    create a big ugly mess.

For convenience, print the output of the failed test job at the end, and rename its trash directory to end with the '.stress-failed' suffix, so it's easy to find in a predictable path (OTOH, all absolute paths recorded in the trash directory become invalid; we'll see whether this causes any issues in practice). If, in an unlikely case, more than one jobs were to fail nearly at the same time, then print the output of all failed jobs, and rename the trash directory of only the last one (i.e. with the highest job number), as it is the trash directory of the test whose output will be at the bottom of the user's terminal.

Based on Jeff King's 'stress' script.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/README                |  19 ++++++-
 t/test-lib-functions.sh |   7 ++-
 t/test-lib.sh           | 109 +++++++++++++++++++++++++++++++++++++++-
 3 files changed, 130 insertions(+), 5 deletions(-)
Show changes to 3 files +130 −5

t/README, t/test-lib-functions.sh, t/test-lib.sh

diff --git a/t/README b/t/README
index 28711cc508..11ce7675e3 100644
--- a/t/README
+++ b/t/README
@@ -186,6 +186,22 @@ appropriately before running "make".
 	this feature by setting the GIT_TEST_CHAIN_LINT environment
 	variable to "1" or "0", respectively.
 
+--stress::
+--stress=<N>::
+	Run the test script repeatedly in multiple parallel jobs until
+	one of them fails.  Useful for reproducing rare failures in
+	flaky tests.  The number of parallel jobs is, in order of
+	precedence: <N>, or the value of the GIT_TEST_STRESS_LOAD
+	environment variable, or twice the number of available
+	processors (as shown by the 'getconf' utility),	or 8.
+	Implies `--verbose -x --immediate` to get the most information
+	about the failure.  Note that the verbose output of each test
+	job is saved to 't/test-results/$TEST_NAME.stress-<nr>.out',
+	and only the output of the failed test job is shown on the
+	terminal.  The names of the trash directories get a
+	'.stress-<nr>' suffix, and the trash directory of the failed
+	test job is renamed to end with a '.stress-failed' suffix.
+
 You can also set the GIT_TEST_INSTALLED environment variable to
 the bindir of an existing git installation to test that installation.
 You still need to have built this git sandbox, from which various
@@ -425,7 +441,8 @@ This test harness library does the following things:
  - Creates an empty test directory with an empty .git/objects database
    and chdir(2) into it.  This directory is 't/trash
    directory.$test_name_without_dotsh', with t/ subject to change by
-   the --root option documented above.
+   the --root option documented above, and a '.stress-<N>' suffix
+   appended by the --stress option.
 
  - Defines standard test helper functions for your scripts to
    use.  These functions are designed to make all scripts behave
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 4459bdda13..92cf8f812c 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1288,8 +1288,6 @@ test_set_port () {
 			# root-only port, use a larger one instead.
 			port=$(($port + 10000))
 		fi
-
-		eval $var=$port
 		;;
 	*[^0-9]*|0*)
 		error >&7 "invalid port number: $port"
@@ -1298,4 +1296,9 @@ test_set_port () {
 		# The user has specified the port.
 		;;
 	esac
+
+	# Make sure that parallel '--stress' test jobs get different
+	# ports.
+	port=$(($port + ${GIT_TEST_STRESS_JOB_NR:-0}))
+	eval $var=$port
 }
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 7d77a26d44..16f74bd524 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -139,6 +139,19 @@ do
 		verbose_log=t
 		tee=t
 		;;
+	--stress)
+		stress=t ;;
+	--stress=*)
+		stress=${opt#--*=}
+		case "$stress" in
+		*[^0-9]*|0*|"")
+			echo "error: --stress=<N> requires the number of jobs to run" >&2
+			exit 1
+			;;
+		*)	# Good.
+			;;
+		esac
+		;;
 	*)
 		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
 	esac
@@ -160,6 +173,13 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$stress"
+then
+	verbose=t
+	trace=t
+	immediate=t
+fi
+
 if test -n "$trace" && test -n "$test_untraceable"
 then
 	# '-x' tracing requested, but this test script can't be reliably
@@ -183,16 +203,101 @@ then
 	verbose=t
 fi
 
+TEST_STRESS_JOB_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
-TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME$TEST_STRESS_JOB_SFX"
+TRASH_DIRECTORY="trash directory.$TEST_NAME$TEST_STRESS_JOB_SFX"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
  *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
 esac
 
+# If --stress was passed, run this test repeatedly in several parallel loops.
+if test "$GIT_TEST_STRESS_STARTED" = "done"
+then
+	: # Don't stress test again.
+elif test -n "$stress"
+then
+	if test "$stress" != t
+	then
+		job_count=$stress
+	elif test -n "$GIT_TEST_STRESS_LOAD"
+	then
+		job_count="$GIT_TEST_STRESS_LOAD"
+	elif job_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null) &&
+	     test -n "$job_count"
+	then
+		job_count=$((2 * $job_count))
+	else
+		job_count=8
+	fi
+
+	mkdir -p "$TEST_RESULTS_DIR"
+	stressfail="$TEST_RESULTS_BASE.stress-failed"
+	rm -f "$stressfail"
+
+	stress_exit=0
+	trap '
+		kill $job_pids 2>/dev/null
+		wait
+		stress_exit=1
+	' TERM INT HUP
+
+	job_pids=
+	job_nr=0
+	while test $job_nr -lt "$job_count"
+	do
+		(
+			GIT_TEST_STRESS_STARTED=done
+			GIT_TEST_STRESS_JOB_NR=$job_nr
+			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
+
+			trap '
+				kill $test_pid 2>/dev/null
+				wait
+				exit 1
+			' TERM INT
+
+			cnt=0
+			while ! test -e "$stressfail"
+			do
+				$TEST_SHELL_PATH "$0" "$@" >"$TEST_RESULTS_BASE.stress-$job_nr.out" 2>&1 &
+				test_pid=$!
+
+				if wait $test_pid
+				then
+					printf "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				else
+					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
+					printf "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				fi
+				cnt=$(($cnt + 1))
+			done
+		) &
+		job_pids="$job_pids $!"
+		job_nr=$(($job_nr + 1))
+	done
+
+	wait
+
+	if test -f "$stressfail"
+	then
+		echo "Log(s) of failed test run(s):"
+		for failed_job_nr in $(sort -n "$stressfail")
+		do
+			echo "Contents of '$TEST_RESULTS_BASE.stress-$failed_job_nr.out':"
+			cat "$TEST_RESULTS_BASE.stress-$failed_job_nr.out"
+		done
+		rm -rf "$TRASH_DIRECTORY.stress-failed"
+		# Move the last one.
+		mv "$TRASH_DIRECTORY.stress-$failed_job_nr" "$TRASH_DIRECTORY.stress-failed"
+	fi
+
+	exit $stress_exit
+fi
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 0/8] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

To recap: this patch series tries to make reproducing rare failures in flaky tests easier: it adds the '--stress' option to our test library to run the test script repeatedly in multiple parallel jobs, in the hope that the increased load creates enough variance in the timing of the test's commands that such a failure is eventually triggered.

There is an issue with v3: by parsing all command line options before re-executing the test script for '--tee' or '--verbose-log', we not only parse the options twice (which is harmless), but check the shell's version for untraceable test scripts twice as well. Consequently, the warning about ignoring '-x' are issued twice:

  $ ./t1510-repo-setup.sh -x --tee
  warning: ignoring -x; './t1510-repo-setup.sh' is untraceable without BASH_XTRACEFD
  warning: ignoring -x; './t1510-repo-setup.sh' is untraceable without BASH_XTRACEFD
  ok 1 - #0: nonbare repo, no explicit configuration
  <...>

Furthermore, when TEST_SHELL_PATH is not /bin/sh but is set to Bash v4.1 or later, then the above command issues one warning as it's run with /bin/sh from the shebang line, but then re-execs itself with Bash and does print the '-x' tracing output as well.

This version of the patch series fixes the above issue by checking the shell's version after '--tee' and '--verbose-log' re-execue the test script. To do so it makes more sense to move the 'test-lib: extract Bash version check for '-x' tracing' patch to the beginning of the series. This move, however, made the range-diff quite noisy, hence the interdiff below, as it describes the changes since v3 much better.

It's based on 'sg/test-bash-version-fix'.
SZEDER Gábor (8):
  test-lib: translate SIGTERM and SIGHUP to an exit
  test-lib: extract Bash version check for '-x' tracing
  test-lib: parse options in a for loop to keep $@ intact
  test-lib: parse command line options earlier
  test-lib: consolidate naming of test-results paths
  test-lib: set $TRASH_DIRECTORY earlier
  test-lib-functions: introduce the 'test_set_port' helper function
  test-lib: add the '--stress' option to run a test repeatedly under
    load
 t/README                 |  19 +-
 t/lib-git-daemon.sh      |   2 +-
 t/lib-git-p4.sh          |   9 +-
 t/lib-git-svn.sh         |   2 +-
 t/lib-httpd.sh           |   2 +-
 t/t0410-partial-clone.sh |   1 -
 t/t5512-ls-remote.sh     |   2 +-
 t/test-lib-functions.sh  |  39 +++++
 t/test-lib.sh            | 365 ++++++++++++++++++++++++++-------------
 9 files changed, 308 insertions(+), 133 deletions(-)
Interdiff:
Show changes to t/test-lib.sh +28 −23
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 3f0b0c07df..a1abb1177a 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -180,29 +180,6 @@ then
 	immediate=t
 fi
 
-if test -n "$trace" && test -n "$test_untraceable"
-then
-	# '-x' tracing requested, but this test script can't be reliably
-	# traced, unless it is run with a Bash version supporting
-	# BASH_XTRACEFD (introduced in Bash v4.1).
-	if test -n "$BASH_VERSION" && eval '
-	     test ${BASH_VERSINFO[0]} -gt 4 || {
-	       test ${BASH_VERSINFO[0]} -eq 4 &&
-	       test ${BASH_VERSINFO[1]} -ge 1
-	     }
-	   '
-	then
-		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
-	else
-		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		trace=
-	fi
-fi
-if test -n "$trace" && test -z "$verbose_log"
-then
-	verbose=t
-fi
-
 TEST_STRESS_JOB_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
@@ -322,6 +299,34 @@ then
 	exit
 fi
 
+if test -n "$trace" && test -n "$test_untraceable"
+then
+	# '-x' tracing requested, but this test script can't be reliably
+	# traced, unless it is run with a Bash version supporting
+	# BASH_XTRACEFD (introduced in Bash v4.1).
+	#
+	# Perform this version check _after_ the test script was
+	# potentially re-executed with $TEST_SHELL_PATH for '--tee' or
+	# '--verbose-log', so the right shell is checked and the
+	# warning is issued only once.
+	if test -n "$BASH_VERSION" && eval '
+	     test ${BASH_VERSINFO[0]} -gt 4 || {
+	       test ${BASH_VERSINFO[0]} -eq 4 &&
+	       test ${BASH_VERSINFO[1]} -ge 1
+	     }
+	   '
+	then
+		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
+	else
+		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		trace=
+	fi
+fi
+if test -n "$trace" && test -z "$verbose_log"
+then
+	verbose=t
+fi
+
 # For repeatability, reset the environment to known value.
 # TERM is sanitized below, after saving color control sequences.
 LANG=C
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 1/8] test-lib: translate SIGTERM and SIGHUP to an exit

Right now if a test script receives SIGTERM or SIGHUP (e.g., because a test was hanging and the user 'kill'-ed it or simply closed the terminal window the test was running in), the shell exits immediately. This can be annoying if the test script did any global setup, like starting apache or git-daemon, as it will not have an opportunity to clean up after itself. A subsequent run of the test won't be able to start its own daemon, and will either fail or skip the tests.

Instead, let's trap SIGTERM and SIGHUP as well to make sure we do a clean shutdown, and just chain it to a normal exit (which will trigger any cleanup).

This patch follows suit of da706545f7 (t: translate SIGINT to an exit, 2015-03-13), and even stole its commit message as well.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Show changes to t/test-lib.sh +1 −1
diff --git a/t/test-lib.sh b/t/test-lib.sh
index c34831a4de..4c3744cce4 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -476,7 +476,7 @@ die () {
 
 GIT_EXIT_OK=
 trap 'die' EXIT
-trap 'exit $?' INT
+trap 'exit $?' INT TERM HUP
 
 # The user-facing functions are loaded from a separate file so that
 # test_perf subshells can have them too
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 2/8] test-lib: extract Bash version check for '-x' tracing

One of our test scripts, 't1510-repo-setup.sh' [1], still can't be reliably run with '-x' tracing enabled, unless it's executed with a Bash version supporting BASH_XTRACEFD (since v4.1). We have a lengthy condition to check the version of the shell running the test script, and disable tracing if it's not executed with a suitable Bash version [2].

Move this check out from the option parsing loop, so other options can imply '-x' by setting 'trace=t', without missing this Bash version check.

[1] 5827506928 (t1510-repo-setup: mark as untraceable with '-x',
    2018-02-24)
[2] 5fc98e79fc (t: add means to disable '-x' tracing for individual
    test scripts, 2018-02-24)
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 37 +++++++++++++++++++------------------
 1 file changed, 19 insertions(+), 18 deletions(-)
Show changes to t/test-lib.sh +19 −18
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 4c3744cce4..1f02e2e25b 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -317,24 +317,7 @@ do
 		GIT_TEST_CHAIN_LINT=0
 		shift ;;
 	-x)
-		# Some test scripts can't be reliably traced  with '-x',
-		# unless the test is run with a Bash version supporting
-		# BASH_XTRACEFD (introduced in Bash v4.1).  Check whether
-		# this test is marked as such, and ignore '-x' if it
-		# isn't executed with a suitable Bash version.
-		if test -z "$test_untraceable" || {
-		     test -n "$BASH_VERSION" && eval '
-		       test ${BASH_VERSINFO[0]} -gt 4 || {
-			 test ${BASH_VERSINFO[0]} -eq 4 &&
-			 test ${BASH_VERSINFO[1]} -ge 1
-		       }
-		     '
-		   }
-		then
-			trace=t
-		else
-			echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		fi
+		trace=t
 		shift ;;
 	-V|--verbose-log)
 		verbose_log=t
@@ -353,6 +336,24 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$trace" && test -n "$test_untraceable"
+then
+	# '-x' tracing requested, but this test script can't be reliably
+	# traced, unless it is run with a Bash version supporting
+	# BASH_XTRACEFD (introduced in Bash v4.1).
+	if test -n "$BASH_VERSION" && eval '
+	     test ${BASH_VERSINFO[0]} -gt 4 || {
+	       test ${BASH_VERSINFO[0]} -eq 4 &&
+	       test ${BASH_VERSINFO[1]} -ge 1
+	     }
+	   '
+	then
+		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
+	else
+		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		trace=
+	fi
+fi
 if test -n "$trace" && test -z "$verbose_log"
 then
 	verbose=t
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 3/8] test-lib: parse options in a for loop to keep $@ intact

'test-lib.sh' looks for the presence of certain options like '--tee' and '--verbose-log', so it can execute the test script again to save its standard output and error, and to do so it needs the original command line options the test was invoked with.

The next patch is about to move the option parsing loop earlier in 'test-lib.sh', but it is implemented using 'shift' in a while loop, effecively destroying "$@" by the end of the option parsing. Not good.

As a preparatory step, turn that option parsing loop into a 'for opt in "$@"' loop to preserve "$@" intact while iterating over the options, and taking extra care to handle the '-r' option's required argument (or the lack thereof).

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 78 +++++++++++++++++++++++++++------------------------
 1 file changed, 42 insertions(+), 36 deletions(-)
Show changes to t/test-lib.sh +42 −36
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 1f02e2e25b..3cf59a92f0 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -264,68 +264,74 @@ test "x$TERM" != "xdumb" && (
 	) &&
 	color=t
 
-while test "$#" -ne 0
+store_arg_to=
+prev_opt=
+for opt
 do
-	case "$1" in
+	if test -n "$store_arg_to"
+	then
+		eval $store_arg_to=\$opt
+		store_arg_to=
+		prev_opt=
+		continue
+	fi
+
+	case "$opt" in
 	-d|--d|--de|--deb|--debu|--debug)
-		debug=t; shift ;;
+		debug=t ;;
 	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
-		immediate=t; shift ;;
+		immediate=t ;;
 	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG; shift ;;
+		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
 	-r)
-		shift; test "$#" -ne 0 || {
-			echo 'error: -r requires an argument' >&2;
-			exit 1;
-		}
-		run_list=$1; shift ;;
+		store_arg_to=run_list
+		;;
 	--run=*)
-		run_list=${1#--*=}; shift ;;
+		run_list=${opt#--*=} ;;
 	-h|--h|--he|--hel|--help)
-		help=t; shift ;;
+		help=t ;;
 	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
-		verbose=t; shift ;;
+		verbose=t ;;
 	--verbose-only=*)
-		verbose_only=${1#--*=}
-		shift ;;
+		verbose_only=${opt#--*=}
+		;;
 	-q|--q|--qu|--qui|--quie|--quiet)
 		# Ignore --quiet under a TAP::Harness. Saying how many tests
 		# passed without the ok/not ok details is always an error.
-		test -z "$HARNESS_ACTIVE" && quiet=t; shift ;;
+		test -z "$HARNESS_ACTIVE" && quiet=t ;;
 	--with-dashes)
-		with_dashes=t; shift ;;
+		with_dashes=t ;;
 	--no-color)
-		color=; shift ;;
+		color= ;;
 	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
-		valgrind=memcheck
-		shift ;;
+		valgrind=memcheck ;;
 	--valgrind=*)
-		valgrind=${1#--*=}
-		shift ;;
+		valgrind=${opt#--*=} ;;
 	--valgrind-only=*)
-		valgrind_only=${1#--*=}
-		shift ;;
+		valgrind_only=${opt#--*=} ;;
 	--tee)
-		shift ;; # was handled already
+		;; # was handled already
 	--root=*)
-		root=${1#--*=}
-		shift ;;
+		root=${opt#--*=} ;;
 	--chain-lint)
-		GIT_TEST_CHAIN_LINT=1
-		shift ;;
+		GIT_TEST_CHAIN_LINT=1 ;;
 	--no-chain-lint)
-		GIT_TEST_CHAIN_LINT=0
-		shift ;;
+		GIT_TEST_CHAIN_LINT=0 ;;
 	-x)
-		trace=t
-		shift ;;
+		trace=t ;;
 	-V|--verbose-log)
-		verbose_log=t
-		shift ;;
+		verbose_log=t ;;
 	*)
-		echo "error: unknown test option '$1'" >&2; exit 1 ;;
+		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
 	esac
+
+	prev_opt=$opt
 done
+if test -n "$store_arg_to"
+then
+	echo "error: $prev_opt requires an argument" >&2
+	exit 1
+fi
 
 if test -n "$valgrind_only"
 then
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 4/8] test-lib: parse command line options earlier

'test-lib.sh' looks for the presence of certain options like '--tee' and '--verbose-log', so it can execute the test script again to save its standard output and error. It looks for '--valgrind' as well, to set up some Valgrind-specific stuff. These all happen before the actual option parsing loop, and the conditions looking for these options look a bit odd, too. They are not completely correct, either, because in a bogus invocation like './t1234-foo.sh -r --tee' they recognize '--tee', although it should be handled as the required argument of the '-r' option. This patch series will add two more options to look out for early, and, in addition, will have to extract these options' stuck arguments (i.e. '--opt=arg') as well.

So let's move the option parsing loop and the couple of related conditions following it earlier in 'test-lib.sh', before the place where the test script is executed again for '--tee' and its friends.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 233 +++++++++++++++++++++++++++-----------------------
 1 file changed, 124 insertions(+), 109 deletions(-)
Show changes to t/test-lib.sh +124 −109
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 3cf59a92f0..14ccb60838 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -71,13 +71,102 @@ then
 	exit 1
 fi
 
+# Parse options while taking care to leave $@ intact, so we will still
+# have all the original command line options when executing the test
+# script again for '--tee' and '--verbose-log' below.
+store_arg_to=
+prev_opt=
+for opt
+do
+	if test -n "$store_arg_to"
+	then
+		eval $store_arg_to=\$opt
+		store_arg_to=
+		prev_opt=
+		continue
+	fi
+
+	case "$opt" in
+	-d|--d|--de|--deb|--debu|--debug)
+		debug=t ;;
+	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
+		immediate=t ;;
+	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
+		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
+	-r)
+		store_arg_to=run_list
+		;;
+	--run=*)
+		run_list=${opt#--*=} ;;
+	-h|--h|--he|--hel|--help)
+		help=t ;;
+	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
+		verbose=t ;;
+	--verbose-only=*)
+		verbose_only=${opt#--*=}
+		;;
+	-q|--q|--qu|--qui|--quie|--quiet)
+		# Ignore --quiet under a TAP::Harness. Saying how many tests
+		# passed without the ok/not ok details is always an error.
+		test -z "$HARNESS_ACTIVE" && quiet=t ;;
+	--with-dashes)
+		with_dashes=t ;;
+	--no-color)
+		color= ;;
+	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
+		valgrind=memcheck
+		tee=t
+		;;
+	--valgrind=*)
+		valgrind=${opt#--*=}
+		tee=t
+		;;
+	--valgrind-only=*)
+		valgrind_only=${opt#--*=}
+		tee=t
+		;;
+	--tee)
+		tee=t ;;
+	--root=*)
+		root=${opt#--*=} ;;
+	--chain-lint)
+		GIT_TEST_CHAIN_LINT=1 ;;
+	--no-chain-lint)
+		GIT_TEST_CHAIN_LINT=0 ;;
+	-x)
+		trace=t ;;
+	-V|--verbose-log)
+		verbose_log=t
+		tee=t
+		;;
+	*)
+		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
+	esac
+
+	prev_opt=$opt
+done
+if test -n "$store_arg_to"
+then
+	echo "error: $prev_opt requires an argument" >&2
+	exit 1
+fi
+
+if test -n "$valgrind_only"
+then
+	test -z "$valgrind" && valgrind=memcheck
+	test -z "$verbose" && verbose_only="$valgrind_only"
+elif test -n "$valgrind"
+then
+	test -z "$verbose_log" && verbose=t
+fi
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
-case "$GIT_TEST_TEE_STARTED, $* " in
-done,*)
-	# do not redirect again
-	;;
-*' --tee '*|*' --va'*|*' -V '*|*' --verbose-log '*)
+if test "$GIT_TEST_TEE_STARTED" = "done"
+then
+	: # do not redirect again
+elif test -n "$tee"
+then
 	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
 	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
 
@@ -94,8 +183,35 @@ done,*)
 	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
 	test "$(cat "$BASE.exit")" = 0
 	exit
-	;;
-esac
+fi
+
+if test -n "$trace" && test -n "$test_untraceable"
+then
+	# '-x' tracing requested, but this test script can't be reliably
+	# traced, unless it is run with a Bash version supporting
+	# BASH_XTRACEFD (introduced in Bash v4.1).
+	#
+	# Perform this version check _after_ the test script was
+	# potentially re-executed with $TEST_SHELL_PATH for '--tee' or
+	# '--verbose-log', so the right shell is checked and the
+	# warning is issued only once.
+	if test -n "$BASH_VERSION" && eval '
+	     test ${BASH_VERSINFO[0]} -gt 4 || {
+	       test ${BASH_VERSINFO[0]} -eq 4 &&
+	       test ${BASH_VERSINFO[1]} -ge 1
+	     }
+	   '
+	then
+		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
+	else
+		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
+		trace=
+	fi
+fi
+if test -n "$trace" && test -z "$verbose_log"
+then
+	verbose=t
+fi
 
 # For repeatability, reset the environment to known value.
 # TERM is sanitized below, after saving color control sequences.
@@ -193,7 +309,7 @@ fi
 
 # Add libc MALLOC and MALLOC_PERTURB test
 # only if we are not executing the test with valgrind
-if expr " $GIT_TEST_OPTS " : ".* --valgrind " >/dev/null ||
+if test -n "$valgrind" ||
    test -n "$TEST_NO_MALLOC_CHECK"
 then
 	setup_malloc_check () {
@@ -264,107 +380,6 @@ test "x$TERM" != "xdumb" && (
 	) &&
 	color=t
 
-store_arg_to=
-prev_opt=
-for opt
-do
-	if test -n "$store_arg_to"
-	then
-		eval $store_arg_to=\$opt
-		store_arg_to=
-		prev_opt=
-		continue
-	fi
-
-	case "$opt" in
-	-d|--d|--de|--deb|--debu|--debug)
-		debug=t ;;
-	-i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate)
-		immediate=t ;;
-	-l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests)
-		GIT_TEST_LONG=t; export GIT_TEST_LONG ;;
-	-r)
-		store_arg_to=run_list
-		;;
-	--run=*)
-		run_list=${opt#--*=} ;;
-	-h|--h|--he|--hel|--help)
-		help=t ;;
-	-v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
-		verbose=t ;;
-	--verbose-only=*)
-		verbose_only=${opt#--*=}
-		;;
-	-q|--q|--qu|--qui|--quie|--quiet)
-		# Ignore --quiet under a TAP::Harness. Saying how many tests
-		# passed without the ok/not ok details is always an error.
-		test -z "$HARNESS_ACTIVE" && quiet=t ;;
-	--with-dashes)
-		with_dashes=t ;;
-	--no-color)
-		color= ;;
-	--va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
-		valgrind=memcheck ;;
-	--valgrind=*)
-		valgrind=${opt#--*=} ;;
-	--valgrind-only=*)
-		valgrind_only=${opt#--*=} ;;
-	--tee)
-		;; # was handled already
-	--root=*)
-		root=${opt#--*=} ;;
-	--chain-lint)
-		GIT_TEST_CHAIN_LINT=1 ;;
-	--no-chain-lint)
-		GIT_TEST_CHAIN_LINT=0 ;;
-	-x)
-		trace=t ;;
-	-V|--verbose-log)
-		verbose_log=t ;;
-	*)
-		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
-	esac
-
-	prev_opt=$opt
-done
-if test -n "$store_arg_to"
-then
-	echo "error: $prev_opt requires an argument" >&2
-	exit 1
-fi
-
-if test -n "$valgrind_only"
-then
-	test -z "$valgrind" && valgrind=memcheck
-	test -z "$verbose" && verbose_only="$valgrind_only"
-elif test -n "$valgrind"
-then
-	test -z "$verbose_log" && verbose=t
-fi
-
-if test -n "$trace" && test -n "$test_untraceable"
-then
-	# '-x' tracing requested, but this test script can't be reliably
-	# traced, unless it is run with a Bash version supporting
-	# BASH_XTRACEFD (introduced in Bash v4.1).
-	if test -n "$BASH_VERSION" && eval '
-	     test ${BASH_VERSINFO[0]} -gt 4 || {
-	       test ${BASH_VERSINFO[0]} -eq 4 &&
-	       test ${BASH_VERSINFO[1]} -ge 1
-	     }
-	   '
-	then
-		: Executed by a Bash version supporting BASH_XTRACEFD.  Good.
-	else
-		echo >&2 "warning: ignoring -x; '$0' is untraceable without BASH_XTRACEFD"
-		trace=
-	fi
-fi
-if test -n "$trace" && test -z "$verbose_log"
-then
-	verbose=t
-fi
-
 if test -n "$color"
 then
 	# Save the color control sequences now rather than run tput
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 5/8] test-lib: consolidate naming of test-results paths

There are two places where we strip off any leading path components and the '.sh' suffix from the test script's pathname, and there are four places where we construct the name of the 't/test-results' directory or the name of various test-specific files in there. The last patch in this series will add even more.

Factor these out into helper variables to avoid repeating ourselves.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)
Show changes to t/test-lib.sh +11 −11
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 14ccb60838..5720292641 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -160,6 +160,10 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+TEST_NAME="$(basename "$0" .sh)"
+TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
@@ -167,12 +171,11 @@ then
 	: # do not redirect again
 elif test -n "$tee"
 then
-	mkdir -p "$TEST_OUTPUT_DIRECTORY/test-results"
-	BASE="$TEST_OUTPUT_DIRECTORY/test-results/$(basename "$0" .sh)"
+	mkdir -p "$TEST_RESULTS_DIR"
 
 	# Make this filename available to the sub-process in case it is using
 	# --verbose-log.
-	GIT_TEST_TEE_OUTPUT_FILE=$BASE.out
+	GIT_TEST_TEE_OUTPUT_FILE=$TEST_RESULTS_BASE.out
 	export GIT_TEST_TEE_OUTPUT_FILE
 
 	# Truncate before calling "tee -a" to get rid of the results
@@ -180,8 +183,8 @@ then
 	>"$GIT_TEST_TEE_OUTPUT_FILE"
 
 	(GIT_TEST_TEE_STARTED=done ${TEST_SHELL_PATH} "$0" "$@" 2>&1;
-	 echo $? >"$BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
-	test "$(cat "$BASE.exit")" = 0
+	 echo $? >"$TEST_RESULTS_BASE.exit") | tee -a "$GIT_TEST_TEE_OUTPUT_FILE"
+	test "$(cat "$TEST_RESULTS_BASE.exit")" = 0
 	exit
 fi
 
@@ -840,12 +843,9 @@ test_done () {
 
 	if test -z "$HARNESS_ACTIVE"
 	then
-		test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results"
-		mkdir -p "$test_results_dir"
-		base=${0##*/}
-		test_results_path="$test_results_dir/${base%.sh}.counts"
+		mkdir -p "$TEST_RESULTS_DIR"
 
-		cat >"$test_results_path" <<-EOF
+		cat >"$TEST_RESULTS_BASE.counts" <<-EOF
 		total $test_count
 		success $test_success
 		fixed $test_fixed
@@ -1051,7 +1051,7 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$(basename "$0" .sh)"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 6/8] test-lib: set $TRASH_DIRECTORY earlier

A later patch in this series will need to know the path to the trash directory early in 'test-lib.sh', but $TRASH_DIRECTORY is set much later.

Set $TRASH_DIRECTORY earlier, where the other test-specific path variables are set.

Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/test-lib.sh | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
Show changes to t/test-lib.sh +6 −6
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 5720292641..0e9ac9118d 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -163,6 +163,12 @@ fi
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
 TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
+TRASH_DIRECTORY="trash directory.$TEST_NAME"
+test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
+case "$TRASH_DIRECTORY" in
+/*) ;; # absolute path is good
+ *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
+esac
 
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
@@ -1051,12 +1057,6 @@ then
 fi
 
 # Test repository
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
-test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
-case "$TRASH_DIRECTORY" in
-/*) ;; # absolute path is good
- *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
-esac
 rm -fr "$TRASH_DIRECTORY" || {
 	GIT_EXIT_OK=t
 	echo >&5 "FATAL: Cannot prepare test area"
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 8/8] test-lib: add the '--stress' option to run a test repeatedly under load

Unfortunately, we have a few flaky tests, whose failures tend to be hard to reproduce. We've found that the best we can do to reproduce such a failure is to run the test script repeatedly while the machine is under load, and wait in the hope that the load creates enough variance in the timing of the test's commands that a failure is evenually triggered. I have a command to do that, and I noticed that two other contributors have rolled their own scripts to do the same, all choosing slightly different approaches.

To help reproduce failures in flaky tests, introduce the '--stress' option to run a test script repeatedly in multiple parallel jobs until one of them fails, thereby using the test script itself to increase the load on the machine.

The number of parallel jobs is determined by, in order of precedence: the number specified as '--stress=<N>', or the value of the GIT_TEST_STRESS_LOAD environment variable, or twice the number of available processors (as reported by the 'getconf' utility), or 8.

Make '--stress' imply '--verbose -x --immediate' to get the most information about rare failures; there is really no point in spending all the extra effort to reproduce such a failure, and then not know which command failed and why.

To prevent the several parallel invocations of the same test from interfering with each other:

  - Include the parallel job's number in the name of the trash
    directory and the various output files under 't/test-results/' as
    a '.stress-<Nr>' suffix.
  - Add the parallel job's number to the port number specified by the
    user or to the test number, so even tests involving daemons
    listening on a TCP socket can be stressed.
  - Redirect each parallel test run's verbose output to
    't/test-results/$TEST_NAME.stress-<nr>.out', because dumping the
    output of several parallel running tests to the terminal would
    create a big ugly mess.

For convenience, print the output of the failed test job at the end, and rename its trash directory to end with the '.stress-failed' suffix, so it's easy to find in a predictable path (OTOH, all absolute paths recorded in the trash directory become invalid; we'll see whether this causes any issues in practice). If, in an unlikely case, more than one jobs were to fail nearly at the same time, then print the output of all failed jobs, and rename the trash directory of only the last one (i.e. with the highest job number), as it is the trash directory of the test whose output will be at the bottom of the user's terminal.

Based on Jeff King's 'stress' script.
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/README                |  19 ++++++-
 t/test-lib-functions.sh |   7 ++-
 t/test-lib.sh           | 109 +++++++++++++++++++++++++++++++++++++++-
 3 files changed, 130 insertions(+), 5 deletions(-)
Show changes to 3 files +130 −5

t/README, t/test-lib-functions.sh, t/test-lib.sh

diff --git a/t/README b/t/README
index 28711cc508..11ce7675e3 100644
--- a/t/README
+++ b/t/README
@@ -186,6 +186,22 @@ appropriately before running "make".
 	this feature by setting the GIT_TEST_CHAIN_LINT environment
 	variable to "1" or "0", respectively.
 
+--stress::
+--stress=<N>::
+	Run the test script repeatedly in multiple parallel jobs until
+	one of them fails.  Useful for reproducing rare failures in
+	flaky tests.  The number of parallel jobs is, in order of
+	precedence: <N>, or the value of the GIT_TEST_STRESS_LOAD
+	environment variable, or twice the number of available
+	processors (as shown by the 'getconf' utility),	or 8.
+	Implies `--verbose -x --immediate` to get the most information
+	about the failure.  Note that the verbose output of each test
+	job is saved to 't/test-results/$TEST_NAME.stress-<nr>.out',
+	and only the output of the failed test job is shown on the
+	terminal.  The names of the trash directories get a
+	'.stress-<nr>' suffix, and the trash directory of the failed
+	test job is renamed to end with a '.stress-failed' suffix.
+
 You can also set the GIT_TEST_INSTALLED environment variable to
 the bindir of an existing git installation to test that installation.
 You still need to have built this git sandbox, from which various
@@ -425,7 +441,8 @@ This test harness library does the following things:
  - Creates an empty test directory with an empty .git/objects database
    and chdir(2) into it.  This directory is 't/trash
    directory.$test_name_without_dotsh', with t/ subject to change by
-   the --root option documented above.
+   the --root option documented above, and a '.stress-<N>' suffix
+   appended by the --stress option.
 
  - Defines standard test helper functions for your scripts to
    use.  These functions are designed to make all scripts behave
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 4459bdda13..92cf8f812c 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1288,8 +1288,6 @@ test_set_port () {
 			# root-only port, use a larger one instead.
 			port=$(($port + 10000))
 		fi
-
-		eval $var=$port
 		;;
 	*[^0-9]*|0*)
 		error >&7 "invalid port number: $port"
@@ -1298,4 +1296,9 @@ test_set_port () {
 		# The user has specified the port.
 		;;
 	esac
+
+	# Make sure that parallel '--stress' test jobs get different
+	# ports.
+	port=$(($port + ${GIT_TEST_STRESS_JOB_NR:-0}))
+	eval $var=$port
 }
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 0e9ac9118d..a1abb1177a 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -139,6 +139,19 @@ do
 		verbose_log=t
 		tee=t
 		;;
+	--stress)
+		stress=t ;;
+	--stress=*)
+		stress=${opt#--*=}
+		case "$stress" in
+		*[^0-9]*|0*|"")
+			echo "error: --stress=<N> requires the number of jobs to run" >&2
+			exit 1
+			;;
+		*)	# Good.
+			;;
+		esac
+		;;
 	*)
 		echo "error: unknown test option '$opt'" >&2; exit 1 ;;
 	esac
@@ -160,16 +173,108 @@ then
 	test -z "$verbose_log" && verbose=t
 fi
 
+if test -n "$stress"
+then
+	verbose=t
+	trace=t
+	immediate=t
+fi
+
+TEST_STRESS_JOB_SFX="${GIT_TEST_STRESS_JOB_NR:+.stress-$GIT_TEST_STRESS_JOB_NR}"
 TEST_NAME="$(basename "$0" .sh)"
 TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/test-results"
-TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME"
-TRASH_DIRECTORY="trash directory.$TEST_NAME"
+TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME$TEST_STRESS_JOB_SFX"
+TRASH_DIRECTORY="trash directory.$TEST_NAME$TEST_STRESS_JOB_SFX"
 test -n "$root" && TRASH_DIRECTORY="$root/$TRASH_DIRECTORY"
 case "$TRASH_DIRECTORY" in
 /*) ;; # absolute path is good
  *) TRASH_DIRECTORY="$TEST_OUTPUT_DIRECTORY/$TRASH_DIRECTORY" ;;
 esac
 
+# If --stress was passed, run this test repeatedly in several parallel loops.
+if test "$GIT_TEST_STRESS_STARTED" = "done"
+then
+	: # Don't stress test again.
+elif test -n "$stress"
+then
+	if test "$stress" != t
+	then
+		job_count=$stress
+	elif test -n "$GIT_TEST_STRESS_LOAD"
+	then
+		job_count="$GIT_TEST_STRESS_LOAD"
+	elif job_count=$(getconf _NPROCESSORS_ONLN 2>/dev/null) &&
+	     test -n "$job_count"
+	then
+		job_count=$((2 * $job_count))
+	else
+		job_count=8
+	fi
+
+	mkdir -p "$TEST_RESULTS_DIR"
+	stressfail="$TEST_RESULTS_BASE.stress-failed"
+	rm -f "$stressfail"
+
+	stress_exit=0
+	trap '
+		kill $job_pids 2>/dev/null
+		wait
+		stress_exit=1
+	' TERM INT HUP
+
+	job_pids=
+	job_nr=0
+	while test $job_nr -lt "$job_count"
+	do
+		(
+			GIT_TEST_STRESS_STARTED=done
+			GIT_TEST_STRESS_JOB_NR=$job_nr
+			export GIT_TEST_STRESS_STARTED GIT_TEST_STRESS_JOB_NR
+
+			trap '
+				kill $test_pid 2>/dev/null
+				wait
+				exit 1
+			' TERM INT
+
+			cnt=0
+			while ! test -e "$stressfail"
+			do
+				$TEST_SHELL_PATH "$0" "$@" >"$TEST_RESULTS_BASE.stress-$job_nr.out" 2>&1 &
+				test_pid=$!
+
+				if wait $test_pid
+				then
+					printf "OK   %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				else
+					echo $GIT_TEST_STRESS_JOB_NR >>"$stressfail"
+					printf "FAIL %2d.%d\n" $GIT_TEST_STRESS_JOB_NR $cnt
+				fi
+				cnt=$(($cnt + 1))
+			done
+		) &
+		job_pids="$job_pids $!"
+		job_nr=$(($job_nr + 1))
+	done
+
+	wait
+
+	if test -f "$stressfail"
+	then
+		echo "Log(s) of failed test run(s):"
+		for failed_job_nr in $(sort -n "$stressfail")
+		do
+			echo "Contents of '$TEST_RESULTS_BASE.stress-$failed_job_nr.out':"
+			cat "$TEST_RESULTS_BASE.stress-$failed_job_nr.out"
+		done
+		rm -rf "$TRASH_DIRECTORY.stress-failed"
+		# Move the last one.
+		mv "$TRASH_DIRECTORY.stress-$failed_job_nr" "$TRASH_DIRECTORY.stress-failed"
+	fi
+
+	exit $stress_exit
+fi
+
 # if --tee was passed, write the output not only to the terminal, but
 # additionally to the file test-results/$BASENAME.out, too.
 if test "$GIT_TEST_TEE_STARTED" = "done"
-- 
2.20.1.151.gec613c4b75
SZEDER Gábor· Jan 5, 2019, 01:08 UTC · re: SZEDER Gábor · lore

[PATCH v4 7/8] test-lib-functions: introduce the 'test_set_port' helper function

Several test scripts run daemons like 'git-daemon' or Apache, and communicate with them through TCP sockets. To have unique ports where these daemons are accessible, the ports are usually the number of the corresponding test scripts, unless the user overrides them via environment variables, and thus all those tests and test libs contain more or less the same bit of one-liner boilerplate code to find out the port. The last patch in this series will make this a bit more complicated.

Factor out finding the port for a daemon into the common helper function 'test_set_port' to avoid repeating ourselves.

Take special care of test scripts with "low" numbers:
  - Test numbers below 1024 would result in a port that's only usable
    as root, so set their port to '10000 + test-nr' to make sure it
    doesn't interfere with other tests in the test suite.  This makes
    the hardcoded port number in 't0410-partial-clone.sh' unnecessary,
    remove it.
  - The shell's arithmetic evaluation interprets numbers with leading
    zeros as octal values, which means that test number below 1000 and
    containing the digits 8 or 9 will trigger an error.  Remove all
    leading zeros from the test numbers to prevent this.

Note that the 'git p4' tests are unlike the other tests involving daemons in that:

  - 'lib-git-p4.sh' doesn't use the test's number for unique port as
    is, but does a bit of additional arithmetic on top [1].
  - The port is not overridable via an environment variable.

With this patch even 'git p4' tests will use the test's number as default port, and it will be overridable via the P4DPORT environment variable.

[1] Commit fc00233071 (git-p4 tests: refactor and cleanup, 2011-08-22)
    introduced that "unusual" unique port computation without
    explaining why it was necessary (as opposed to simply using the
    test number as is).  It seems to be just unnecessary complication,
    and in any case that commit came way before the "test nr as unique
    port" got "standardized" for other daemons in commits c44132fcf3
    (tests: auto-set git-daemon port, 2014-02-10), 3bb486e439 (tests:
    auto-set LIB_HTTPD_PORT from test name, 2014-02-10), and
    bf9d7df950 (t/lib-git-svn.sh: improve svnserve tests with parallel
    make test, 2017-12-01).
Signed-off-by: SZEDER Gábor <szeder.dev@gmail.com>
---
 t/lib-git-daemon.sh      |  2 +-
 t/lib-git-p4.sh          |  9 +--------
 t/lib-git-svn.sh         |  2 +-
 t/lib-httpd.sh           |  2 +-
 t/t0410-partial-clone.sh |  1 -
 t/t5512-ls-remote.sh     |  2 +-
 t/test-lib-functions.sh  | 36 ++++++++++++++++++++++++++++++++++++
 7 files changed, 41 insertions(+), 13 deletions(-)
Show changes to 7 files +41 −13

t/lib-git-daemon.sh, t/lib-git-p4.sh, t/lib-git-svn.sh, t/lib-httpd.sh, t/t0410-partial-clone.sh, t/t5512-ls-remote.sh, t/test-lib-functions.sh

diff --git a/t/lib-git-daemon.sh b/t/lib-git-daemon.sh
index f98de95c15..41eb1e3ae8 100644
--- a/t/lib-git-daemon.sh
+++ b/t/lib-git-daemon.sh
@@ -28,7 +28,7 @@ then
 	test_skip_or_die $GIT_TEST_GIT_DAEMON "file system does not support FIFOs"
 fi
 
-LIB_GIT_DAEMON_PORT=${LIB_GIT_DAEMON_PORT-${this_test#t}}
+test_set_port LIB_GIT_DAEMON_PORT
 
 GIT_DAEMON_PID=
 GIT_DAEMON_DOCUMENT_ROOT_PATH="$PWD"/repo
diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh
index c27599474c..b3be3ba011 100644
--- a/t/lib-git-p4.sh
+++ b/t/lib-git-p4.sh
@@ -53,14 +53,7 @@ time_in_seconds () {
 	(cd / && "$PYTHON_PATH" -c 'import time; print(int(time.time()))')
 }
 
-# Try to pick a unique port: guess a large number, then hope
-# no more than one of each test is running.
-#
-# This does not handle the case where somebody else is running the
-# same tests and has chosen the same ports.
-testid=${this_test#t}
-git_p4_test_start=9800
-P4DPORT=$((10669 + ($testid - $git_p4_test_start)))
+test_set_port P4DPORT
 
 P4PORT=localhost:$P4DPORT
 P4CLIENT=client
diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh
index a8130f9119..f3b478c307 100644
--- a/t/lib-git-svn.sh
+++ b/t/lib-git-svn.sh
@@ -13,6 +13,7 @@ fi
 GIT_DIR=$PWD/.git
 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
@@ -119,7 +120,6 @@ require_svnserve () {
 }
 
 start_svnserve () {
-	SVNSERVE_PORT=${SVNSERVE_PORT-${this_test#t}}
 	svnserve --listen-port $SVNSERVE_PORT \
 		 --root "$rawsvnrepo" \
 		 --listen-once \
diff --git a/t/lib-httpd.sh b/t/lib-httpd.sh
index a8729f8232..e465116ef9 100644
--- a/t/lib-httpd.sh
+++ b/t/lib-httpd.sh
@@ -82,7 +82,7 @@ case $(uname) in
 esac
 
 LIB_HTTPD_PATH=${LIB_HTTPD_PATH-"$DEFAULT_HTTPD_PATH"}
-LIB_HTTPD_PORT=${LIB_HTTPD_PORT-${this_test#t}}
+test_set_port LIB_HTTPD_PORT
 
 TEST_PATH="$TEST_DIRECTORY"/lib-httpd
 HTTPD_ROOT_PATH="$PWD"/httpd
diff --git a/t/t0410-partial-clone.sh b/t/t0410-partial-clone.sh
index ba3887f178..0aca8d7588 100755
--- a/t/t0410-partial-clone.sh
+++ b/t/t0410-partial-clone.sh
@@ -480,7 +480,6 @@ test_expect_success 'gc stops traversal when a missing but promised object is re
 	! grep "$TREE_HASH" out
 '
 
-LIB_HTTPD_PORT=12345  # default port, 410, cannot be used as non-root
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 32e722db2e..cd9e60632d 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -260,7 +260,7 @@ test_lazy_prereq GIT_DAEMON '
 # This test spawns a daemon, so run it only if the user would be OK with
 # testing with git-daemon.
 test_expect_success PIPE,JGIT,GIT_DAEMON 'indicate no refs in standards-compliant empty remote' '
-	JGIT_DAEMON_PORT=${JGIT_DAEMON_PORT-${this_test#t}} &&
+	test_set_port JGIT_DAEMON_PORT &&
 	JGIT_DAEMON_PID= &&
 	git init --bare empty.git &&
 	>empty.git/git-daemon-export-ok &&
diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh
index 6b3bbf99e4..4459bdda13 100644
--- a/t/test-lib-functions.sh
+++ b/t/test-lib-functions.sh
@@ -1263,3 +1263,39 @@ test_oid () {
 	fi &&
 	eval "printf '%s' \"\${$var}\""
 }
+
+# Choose a port number based on the test script's number and store it in
+# the given variable name, unless that variable already contains a number.
+test_set_port () {
+	local var=$1 port
+
+	if test $# -ne 1 || test -z "$var"
+	then
+		BUG "test_set_port requires a variable name"
+	fi
+
+	eval port=\$$var
+	case "$port" in
+	"")
+		# No port is set in the given env var, use the test
+		# number as port number instead.
+		# Remove not only the leading 't', but all leading zeros
+		# as well, so the arithmetic below won't (mis)interpret
+		# a test number like '0123' as an octal value.
+		port=${this_test#${this_test%%[1-9]*}}
+		if test "${port:-0}" -lt 1024
+		then
+			# root-only port, use a larger one instead.
+			port=$(($port + 10000))
+		fi
+
+		eval $var=$port
+		;;
+	*[^0-9]*|0*)
+		error >&7 "invalid port number: $port"
+		;;
+	*)
+		# The user has specified the port.
+		;;
+	esac
+}
-- 
2.20.1.151.gec613c4b75
Jeff King· Jan 7, 2019, 08:49 UTC · re: SZEDER Gábor · lore

Re: [PATCH v4 0/8] test-lib: add the '--stress' option to help reproduce occasional failures in flaky tests

On Sat, Jan 05, 2019 at 02:08:51AM +0100, SZEDER Gábor wrote:
Show 5 quoted lines
> To recap: this patch series tries to make reproducing rare failures in
> flaky tests easier: it adds the '--stress' option to our test library
> to run the test script repeatedly in multiple parallel jobs, in the
> hope that the increased load creates enough variance in the timing of
> the test's commands that such a failure is eventually triggered.

Thanks, I looked over this version (glossing over the bits that hadn't really changed since v2) and it all looks good to me.

-Peff

← back to recent threads