From: Jeff King <hidden> Date: 2025-06-23 10:55:17
This is a small quality of life improvement for our test suite, stemming
from this discussion:
https://lore.kernel.org/git/20240408172638.GB1629595@coredump.intra.peff.net/
But somehow it took me over a year to get back to it. :-/
The first patch is a small cleanup in nearby code; the second one is the
interesting part.
[1/2]: t7422: replace confusing printf with echo
[2/2]: test-lib: teach test_seq the -f option
t/t0021-conversion.sh | 4 ++--
t/t0610-reftable-basics.sh | 6 +-----
t/t0612-reftable-jgit-compatibility.sh | 13 +++++--------
t/t0613-reftable-write-options.sh | 24 ++++--------------------
t/t1400-update-ref.sh | 10 ++--------
t/t5004-archive-corner-cases.sh | 5 +----
t/t6422-merge-rename-corner-cases.sh | 10 ++--------
t/t7422-submodule-output.sh | 9 +++------
t/test-lib-functions.sh | 9 ++++++++-
9 files changed, 28 insertions(+), 62 deletions(-)
-Peff
From: Jeff King <hidden> Date: 2025-06-23 10:55:44
While looping over a counter "i", we do:
printf "[submodule \"sm-$i\"]\npath = recursive-submodule-path-$i\n" "$i"
So we are passing "$i" as an argument to be filled in, but there is no
"%" placeholder in the format string, which is a bit confusing to read.
We could switch both instances of "$i" to "%d" (and pass $i twice). But
that makes the line even longer. Let's just keep interpolating the value
in the string, and drop the confusing extra "$i" argument.
And since we are not using any printf specifiers at all, it becomes
clear that we can swap it out for echo. We do use a "\n" in the middle
of the string, but breaking this into two separate echo statements
actually makes it easier to read.
Signed-off-by: Jeff King <redacted>
---
t/t7422-submodule-output.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
From: Jeff King <hidden> Date: 2025-06-23 10:56:26
The "seq" tool has a "-f" option to produce printf-style formatted
lines. Let's teach our test_seq helper the same trick. This lets us get
rid of some shell loops in test snippets (which are particularly verbose
in our test suite because we have to "|| return 1" to keep the &&-chain
going).
This converts a few call-sites I found by grepping around the test
suite. A few notes on these:
- In "seq", the format specifier is a "%g" float. Since test_seq only
supports integers, I've kept the more natural "%d" (which is what
these call sites were using already).
- Like "seq", test_seq automatically adds a newline to the specified
format. This is what all callers are doing already except for t0021,
but there we do not care about the exact format. We are just trying
to printf a large number of bytes to a file. It's not worth
complicating other callers or adding an option to avoid the newline
in that caller.
- Most conversions are just replacing a shell loop (which does get rid
of an extra fork, since $() requires a subshell). In t0612 we can
replace an awk invocation, which I think makes the end result more
readable, as there's less quoting.
- In t7422 we can replace one loop, but sadly we have to leave the
loop directly above it. This is because that earlier loop wants to
include the seq value twice in the output, which test_seq does not
support (nor does regular seq). If you run:
test_seq -f "foo-%d %d" 10
the second "%d" will always be the empty string. You might naively
think that test_seq could add some extra arguments, like:
# 3 ought to be enough for anyone...
printf "$fmt\n" "$i "$i" $i"
but that just triggers printf to format multiple lines, one per
extra set of arguments.
So we'd have to actually parse the format string, figure out how
many "%" placeholders are there, and then feed it that many
instances of the sequence number. The complexity isn't worth it.
Signed-off-by: Jeff King <redacted>
---
t/t0021-conversion.sh | 4 ++--
t/t0610-reftable-basics.sh | 6 +-----
t/t0612-reftable-jgit-compatibility.sh | 13 +++++--------
t/t0613-reftable-write-options.sh | 24 ++++--------------------
t/t1400-update-ref.sh | 10 ++--------
t/t5004-archive-corner-cases.sh | 5 +----
t/t6422-merge-rename-corner-cases.sh | 10 ++--------
t/t7422-submodule-output.sh | 6 +-----
t/test-lib-functions.sh | 9 ++++++++-
9 files changed, 26 insertions(+), 61 deletions(-)
@@ -281,7 +281,7 @@ test_expect_success 'required filter with absent smudge field' ' test_expect_success'filtering large input to small output should use little memory''test_configfilter.devnull.clean"cat >/dev/null"&&test_configfilter.devnull.requiredtrue&&-foriin$(test_seq130);doprintf"%1048576d"1||return1;done>30MB&&+test_seq-f"%1048576d"130>30MB&&echo"30MB filter=devnull">.gitattributes&&GIT_MMAP_LIMIT=1mGIT_ALLOC_LIMIT=1mgitadd30MB'
@@ -299,7 +299,7 @@ test_expect_success 'filter that does not read is fine' ' test_expect_successEXPENSIVE'filter large file''test_configfilter.largefile.smudgecat&&test_configfilter.largefile.cleancat&&-foriin$(test_seq12048);doprintf"%1048576d"1||return1;done>2GB&&+test_seq-f"%1048576d"12048>2GB&&echo"2GB filter=largefile">.gitattributes&&gitadd2GB2>err&&test_must_be_emptyerr&&
@@ -180,11 +176,7 @@ test_expect_success 'restart interval at every single record' '(cdrepo&&test_commitinitial&&-foriin$(test_seq10)-do-printf"update refs/heads/branch-%d HEAD\n""$i"||-return1-done>input&&+test_seq-f"update refs/heads/branch-%d HEAD"10>input&&gitupdate-ref--stdin<input&&git-creftable.restartInterval=1pack-refs&&
@@ -224,11 +216,7 @@ test_expect_success 'object index gets written by default with ref index' '(cdrepo&&test_commitinitial&&-foriin$(test_seq5)-do-printf"update refs/heads/branch-%d HEAD\n""$i"||-return1-done>input&&+test_seq-f"update refs/heads/branch-%d HEAD"5>input&&gitupdate-ref--stdin<input&&git-creftable.blockSize=100pack-refs&&
@@ -263,11 +251,7 @@ test_expect_success 'object index can be disabled' '(cdrepo&&test_commitinitial&&-foriin$(test_seq5)-do-printf"update refs/heads/branch-%d HEAD\n""$i"||-return1-done>input&&+test_seq-f"update refs/heads/branch-%d HEAD"5>input&&gitupdate-ref--stdin<input&&git-creftable.blockSize=100-creftable.indexObjects=falsepack-refs&&
@@ -176,10 +176,7 @@ test_expect_success EXPENSIVE,UNZIP,UNZIP_ZIP64_SUPPORT \blob=$(echo$s|githash-object-w--stdin)&&# create tree containing 65500 entries of that blob-foriin$(test_seq165500)-do-echo"100644 blob $blob$i"||return1-done>tree&&+test_seq-f"100644 blob $blob\t%d"165500>tree&&tree=$(gitmktree<tree)&&# zip it, creating an archive a bit bigger than 4GB
@@ -1146,10 +1146,7 @@ test_conflicts_with_adds_and_renames() {cdsimple_${sideL}_${sideR}&&# Create some related files now-foriin$(test_seq110)-do-echoRandombasecontentline$i-done>file_v1&&+test_seq-f"Random base content line %d"110>file_v1&&cpfile_v1file_v2&&echomodification>>file_v2&&
@@ -1293,10 +1290,7 @@ test_setup_nested_conflicts_from_rename_rename () {cdnested_conflicts_from_rename_rename&&# Create some related files now-foriin$(test_seq110)-do-echoRandombasecontentline$i-done>file_v1&&+test_seq-f"Random base content line %d"110>file_v1&&cpfile_v1file_v2&&cpfile_v1file_v3&&
The "seq" tool has a "-f" option to produce printf-style formatted
lines. Let's teach our test_seq helper the same trick. This lets us get
rid of some shell loops in test snippets (which are particularly verbose
in our test suite because we have to "|| return 1" to keep the &&-chain
going).
This converts a few call-sites I found by grepping around the test
suite. A few notes on these:
- In "seq", the format specifier is a "%g" float. Since test_seq only
supports integers, I've kept the more natural "%d" (which is what
these call sites were using already).
Sticking with "%d" definately feels more natural.
quoted hunk
- Like "seq", test_seq automatically adds a newline to the specified
format. This is what all callers are doing already except for t0021,
but there we do not care about the exact format. We are just trying
to printf a large number of bytes to a file. It's not worth
complicating other callers or adding an option to avoid the newline
in that caller.
- Most conversions are just replacing a shell loop (which does get rid
of an extra fork, since $() requires a subshell). In t0612 we can
replace an awk invocation, which I think makes the end result more
readable, as there's less quoting.
- In t7422 we can replace one loop, but sadly we have to leave the
loop directly above it. This is because that earlier loop wants to
include the seq value twice in the output, which test_seq does not
support (nor does regular seq). If you run:
test_seq -f "foo-%d %d" 10
the second "%d" will always be the empty string. You might naively
think that test_seq could add some extra arguments, like:
# 3 ought to be enough for anyone...
printf "$fmt\n" "$i "$i" $i"
but that just triggers printf to format multiple lines, one per
extra set of arguments.
So we'd have to actually parse the format string, figure out how
many "%" placeholders are there, and then feed it that many
instances of the sequence number. The complexity isn't worth it.
Signed-off-by: Jeff King <redacted>
---
t/t0021-conversion.sh | 4 ++--
t/t0610-reftable-basics.sh | 6 +-----
t/t0612-reftable-jgit-compatibility.sh | 13 +++++--------
t/t0613-reftable-write-options.sh | 24 ++++--------------------
t/t1400-update-ref.sh | 10 ++--------
t/t5004-archive-corner-cases.sh | 5 +----
t/t6422-merge-rename-corner-cases.sh | 10 ++--------
t/t7422-submodule-output.sh | 6 +-----
t/test-lib-functions.sh | 9 ++++++++-
9 files changed, 26 insertions(+), 61 deletions(-)
@@ -281,7 +281,7 @@ test_expect_success 'required filter with absent smudge field' ' test_expect_success'filtering large input to small output should use little memory''test_configfilter.devnull.clean"cat >/dev/null"&&test_configfilter.devnull.requiredtrue&&-foriin$(test_seq130);doprintf"%1048576d"1||return1;done>30MB&&+test_seq-f"%1048576d"130>30MB&&
@@ -1454,6 +1454,13 @@ test_cmp_fspath () {# from 1. test_seq(){+localfmt="%d"+case"$1"in+-f)+fmt="$2"
With the `-f` option, the default format string gets overwritten to what
is provided by the user. Makes sense.
If we want, we could update the comment above this function to mention
this new option.
quoted hunk
+ shift 2
+ ;;
+ esac
case $# in
1) set 1 "$@" ;;
2) ;;
@@ -1462,7 +1469,7 @@ test_seq () { test_seq_counter__=$1 while test "$test_seq_counter__" -le "$2" do- echo "$test_seq_counter__"+ printf "$fmt\n" "$test_seq_counter__"
Nice and simple! Each of the updated callsites also look good to me.
-Justin
The "seq" tool has a "-f" option to produce printf-style formatted
lines. Let's teach our test_seq helper the same trick. This lets us get
rid of some shell loops in test snippets (which are particularly verbose
in our test suite because we have to "|| return 1" to keep the &&-chain
going).
Is it a sharp edge worth caring about that someone might
write `test_seq -f 1 5` where we'd pass 1 as the format
string?
If so, perhaps a check like this might be sufficient to
catch it early?
diff --git i/t/test-lib-functions.sh w/t/test-lib-functions.sh
index 8c176f4efc..87b59d5895 100644
--- i/t/test-lib-functions.sh
+++ w/t/test-lib-functions.sh
@@ -1458,6 +1458,10 @@ test_seq () {
case "$1" in
-f)
fmt="$2"
+ case "$fmt" in
+ *%*) : ;;
+ *) BUG "no % in -f argument" ;;
+ esac
shift 2
;;
esac
I don't know whether it's worth the extra code or not. I
just wondered about how it would fail in the face of a minor
typo. It certainly should cause any test to fail if it were
to output 1 instead of the intended format string, so it's
arguably fine as-is.
Adding -f to the usage note above, as Justin suggested might
help folks avoid making the mistake of cuddling the format
string against -f, e.g.: -f%d. That is caught by the
parameter count check (though perhaps not everyone would
notice why, thinking they did pass an argument to -f).
--
Todd
From: Eric Sunshine <hidden> Date: 2025-06-23 18:00:00
On Mon, Jun 23, 2025 at 6:57 AM Jeff King [off-list ref] wrote:
quoted hunk
While looping over a counter "i", we do:
printf "[submodule \"sm-$i\"]\npath = recursive-submodule-path-$i\n" "$i"
So we are passing "$i" as an argument to be filled in, but there is no
"%" placeholder in the format string, which is a bit confusing to read.
We could switch both instances of "$i" to "%d" (and pass $i twice). But
that makes the line even longer. Let's just keep interpolating the value
in the string, and drop the confusing extra "$i" argument.
And since we are not using any printf specifiers at all, it becomes
clear that we can swap it out for echo. We do use a "\n" in the middle
of the string, but breaking this into two separate echo statements
actually makes it easier to read.
Signed-off-by: Jeff King <redacted>
---
This looks obviously correct and, as the commit message says, is
almost certainly easier to read, but I was more than a little
surprised to see the patch since I thought this code had been fixed
previously[*] and had some discussion around it.
[*] https://lore.kernel.org/git/20250403144852.19153-1-sn03.general@gmail.com/
From: Eric Sunshine <hidden> Date: 2025-06-24 06:22:33
On Mon, Jun 23, 2025 at 6:56 AM Jeff King [off-list ref] wrote:
quoted hunk
The "seq" tool has a "-f" option to produce printf-style formatted
lines. Let's teach our test_seq helper the same trick. This lets us get
rid of some shell loops in test snippets (which are particularly verbose
in our test suite because we have to "|| return 1" to keep the &&-chain
going).
Signed-off-by: Jeff King <redacted>
---
I had suggested[1] an effectively equivalent change to Patrick for a
couple tests in the nearby t0610, but he rejected[2] the idea due to
the pure-shell version being significantly slower than the `awk`
version.
Pondering his response today, I wondered if it would make sense to
replace our pure-shell `test_seq` with an implementation via `awk`,
however, if most of our sequence vend only a small set of numbers,
then the startup cost of `awk` would probably swamp any savings,
especially on Windows where process startup is extremely slow. Taking
that into account, I further wondered if we could see an overall win
by taking a hybrid approach in which we employ the pure-shell version
if vending a small set of numbers, but fall over to an `awk` version
if vending a lot of numbers, especially as in the test above or the
tests in t0610. Anyhow, food for thought, or not, if you're not hungry
for thought food.
[1]: https://lore.kernel.org/git/CAPig+cSC3zdur1fCsa7RMNZDcgUK4pUGKb22tpgdANxR6OxNMA@mail.gmail.com/
[2]: https://lore.kernel.org/git/Z-FUNgmY9hTsnzds@pks.im/
This looks obviously correct and, as the commit message says, is
almost certainly easier to read, but I was more than a little
surprised to see the patch since I thought this code had been fixed
previously[*] and had some discussion around it.
[*] https://lore.kernel.org/git/20250403144852.19153-1-sn03.general@gmail.com/
Ah, interesting, I hadn't seen that one. I am happy with either
solution, but IMHO what I posted with "echo" is a bit more readable.
-Peff
@@ -1454,6 +1454,13 @@ test_cmp_fspath () {# from 1. test_seq(){+localfmt="%d"+case"$1"in+-f)+fmt="$2"
With the `-f` option, the default format string gets overwritten to what
is provided by the user. Makes sense.
If we want, we could update the comment above this function to mention
this new option.
Good point. I didn't even notice that comment!
Perhaps we should squash this in? I don't think there's any need to keep
it as a separate commit.
-- >8 --
Subject: [PATCH] test-lib: document test_seq's "-f" option
The previous commit added the "-f" option, but didn't mention it in the
function's documentation.
Suggested-by: Justin Tobler <redacted>
Signed-off-by: Jeff King <redacted>
---
t/test-lib-functions.sh | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
@@ -1451,7 +1451,12 @@ test_cmp_fspath () {# test_seq 1 5 -- outputs 1 2 3 4 5 one line at a time## or with one argument (end), in which case it starts counting-# from 1.+# from 1. In addition to the start/end arguments, you can pass an optional+# printf format. For example:+#+# test_seq -f "line %d" 1 5+#+# would print 5 lines, "line 1" through "line 5". test_seq(){localfmt="%d"
From: Jeff King <hidden> Date: 2025-06-24 10:16:53
On Mon, Jun 23, 2025 at 01:27:07PM -0400, Todd Zullinger wrote:
Is it a sharp edge worth caring about that someone might
write `test_seq -f 1 5` where we'd pass 1 as the format
string?
If so, perhaps a check like this might be sufficient to
catch it early?
diff --git i/t/test-lib-functions.sh w/t/test-lib-functions.sh
index 8c176f4efc..87b59d5895 100644
--- i/t/test-lib-functions.sh
+++ w/t/test-lib-functions.sh
@@ -1458,6 +1458,10 @@ test_seq () {
case "$1" in
-f)
fmt="$2"
+ case "$fmt" in
+ *%*) : ;;
+ *) BUG "no % in -f argument" ;;
+ esac
shift 2
;;
esac
I don't know whether it's worth the extra code or not. I
just wondered about how it would fail in the face of a minor
typo. It certainly should cause any test to fail if it were
to output 1 instead of the intended format string, so it's
arguably fine as-is.
Hmm, maybe. I notice that "seq" itself does this (though it did surprise
me). I think there it is actually doing the "%" interpolation itself (to
avoid memory errors by feeding arbitrary strings to printf functions),
so it's easy to do.
In our case, we can rely on the shell printf to do something sensible if
fed garbage. And because we're not parsing ourselves, a pattern like you
have above isn't totally accurate (e.g., consider what it would with
"%%d"). But it probably would be enough to catch typos.
It would also disallow:
test_seq -f "same line" 50
to produce repeated lines, though I don't know how valuable that would
be. So I dunno.
Adding -f to the usage note above, as Justin suggested might
help folks avoid making the mistake of cuddling the format
string against -f, e.g.: -f%d. That is caught by the
parameter count check (though perhaps not everyone would
notice why, thinking they did pass an argument to -f).
If people are going to use "-f%d", I think we'd be better off making it
work than trying to complain about it. But I was hoping we could just
keep things simple and stupid, given the limited audience.
So my inclination is to leave the sharp edges and see if anybody gets
cut, but it's possible that I'm just being lazy.
-Peff
I had suggested[1] an effectively equivalent change to Patrick for a
couple tests in the nearby t0610, but he rejected[2] the idea due to
the pure-shell version being significantly slower than the `awk`
version.
Pondering his response today, I wondered if it would make sense to
replace our pure-shell `test_seq` with an implementation via `awk`,
however, if most of our sequence vend only a small set of numbers,
then the startup cost of `awk` would probably swamp any savings,
especially on Windows where process startup is extremely slow. Taking
that into account, I further wondered if we could see an overall win
by taking a hybrid approach in which we employ the pure-shell version
if vending a small set of numbers, but fall over to an `awk` version
if vending a lot of numbers, especially as in the test above or the
tests in t0610. Anyhow, food for thought, or not, if you're not hungry
for thought food.
Ah, interesting. I didn't time it at all, as my general intuition for
shell performance is that counting process spawns overrides everything
else (though admittedly it is usually O(n) processes vs O(1), and here
we are going from one extra process to zero).
I did a few timings, and it looks like the shell wins at 10,000 on my
system, but awk wins at 50,000 (though there is a lot of run-to-run
noise; I think awk might even win at 10,000 on a loaded system, as this
is such a light load that CPU frequency throttling comes into play).
I assumed that the culprit was a lack of buffering, but I don't think
so. awk seems to issue 10,000 write() calls. I guess it is just internal
shell overhead in issuing commands. Where is a JIT byte-code shell
interpreter when we need one? ;)
My inclination is not to worry about it too much. At 10,000 I think we
are talking about a few milliseconds. There's so much more low-hanging
fruit if somebody wants to optimize the test suite. IMHO readability is
more important here (and if we really want to optimize, doing it inside
test_seq would be better).
-Peff
On Mon, Jun 23, 2025 at 01:27:07PM -0400, Todd Zullinger wrote:
quoted
I don't know whether it's worth the extra code or not. I
just wondered about how it would fail in the face of a minor
typo. It certainly should cause any test to fail if it were
to output 1 instead of the intended format string, so it's
arguably fine as-is.
Hmm, maybe. I notice that "seq" itself does this (though it did surprise
me). I think there it is actually doing the "%" interpolation itself (to
avoid memory errors by feeding arbitrary strings to printf functions),
so it's easy to do.
In our case, we can rely on the shell printf to do something sensible if
fed garbage. And because we're not parsing ourselves, a pattern like you
have above isn't totally accurate (e.g., consider what it would with
"%%d"). But it probably would be enough to catch typos.
Parsing it fully felt like overkill, and a good bit more
code to do it well. So that was me being lazy. :)
It would also disallow:
test_seq -f "same line" 50
to produce repeated lines, though I don't know how valuable that would
be. So I dunno.
I can see someone using it that way, like a cheap version of
the yes command.
If people are going to use "-f%d", I think we'd be better off making it
work than trying to complain about it. But I was hoping we could just
keep things simple and stupid, given the limited audience.
So my inclination is to leave the sharp edges and see if anybody gets
cut, but it's possible that I'm just being lazy.
Sounds good to me. As you say, it's not going to be exposed
to "normal" users.
The worst thing it can do is allow a test to pass which
shouldn't -- and that's by far the least likely way for the
(not very) sharp edges to cut us.
--
Todd
@@ -1454,6 +1454,13 @@ test_cmp_fspath () {# from 1. test_seq(){+localfmt="%d"+case"$1"in+-f)+fmt="$2"
With the `-f` option, the default format string gets overwritten to what
is provided by the user. Makes sense.
If we want, we could update the comment above this function to mention
this new option.
Good point. I didn't even notice that comment!
Perhaps we should squash this in? I don't think there's any need to keep
it as a separate commit.
Ya, that seems reasonable to me.
quoted hunk
-- >8 --
Subject: [PATCH] test-lib: document test_seq's "-f" option
The previous commit added the "-f" option, but didn't mention it in the
function's documentation.
Suggested-by: Justin Tobler <redacted>
Signed-off-by: Jeff King <redacted>
---
t/test-lib-functions.sh | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
@@ -1451,7 +1451,12 @@ test_cmp_fspath () {# test_seq 1 5 -- outputs 1 2 3 4 5 one line at a time## or with one argument (end), in which case it starts counting-# from 1.+# from 1. In addition to the start/end arguments, you can pass an optional+# printf format. For example:+#+# test_seq -f "line %d" 1 5+#+# would print 5 lines, "line 1" through "line 5".
At first I thought it might be nice to mention that only format strings with
a single specifier are supported, but I think this can also be implied
since the comment mentions the format string follows the printf format.
This looks good to me :)
-Justin
test_seq () {
local fmt="%d"
--
2.50.0.399.g566d3d7b27