t/chainlint: add more chainlint.pl self-tests

During the development of chainlint.pl, numerous new self-tests were created to verify correct functioning beyond the checks already represented by the existing self-tests. The new checks fall into several categories: * behavior of the lexical analyzer for complex cases, such as line splicing, token pasting, entering and exiting string contexts inside and outside of test script bodies; for instance: test_expect_success 'title' ' x=$(echo "something" | sed -e '\''s/\\/\\\\/g'\'' -e '\''s/[[/.*^$]/\\&/g'\'' ' * behavior of the parser for all compound grammatical constructs, such as `if...fi`, `case...esac`, `while...done`, `{...}`, etc., and for other legal shell grammatical constructs not covered by existing chainlint.sed self-tests, as well as complex cases, such as: OUT=$( ((large_git 1>&3) | :) 3>&1 ) && * detection of problems, such as &&-chain breakage, from top-level to any depth since the existing self-tests do not cover any top-level context and only cover subshells one level deep due to limitations of chainlint.sed * address blind spots in chainlint.sed (such as not detecting a broken &&-chain on a one-line for-loop in a subshell[1]) which chainlint.pl correctly detects * real-world cases which tripped up chainlint.pl during its development [1]: https://lore.kernel.org/git/dce35a47012fecc6edc11c68e91dbb485c5bc36f.1661663880.git.gitgitgadget@gmail.com/ Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Eric Sunshine committed Sep 1, 2022 at 00:29 UTC 56066523ed3ebd16b455e99ce954ec19b6ac5ada
34 files changed +336 -2
t/chainlint/blank-line-before-esac.expect new
+18
@@ -0,0 +1,18 @@
1 +test_done ( ) {
2 + case "$test_failure" in
3 + 0 )
4 + test_at_end_hook_
5 +
6 + exit 0 ;;
7 +
8 + * )
9 + if test $test_external_has_tap -eq 0
10 + then
11 + say_color error "# failed $test_failure among $msg"
12 + say "1..$test_count"
13 + fi
14 +
15 + exit 1 ;;
16 +
17 + esac
18 +}
t/chainlint/blank-line-before-esac.test new
+19
@@ -0,0 +1,19 @@
1 +# LINT: blank line before "esac"
2 +test_done () {
3 + case "$test_failure" in
4 + 0)
5 + test_at_end_hook_
6 +
7 + exit 0 ;;
8 +
9 + *)
10 + if test $test_external_has_tap -eq 0
11 + then
12 + say_color error "# failed $test_failure among $msg"
13 + say "1..$test_count"
14 + fi
15 +
16 + exit 1 ;;
17 +
18 + esac
19 +}
t/chainlint/block.expect
+12 -1
@@ -9,4 +9,15 @@
9 echo c
10 } ?!AMP?!
11 baz
12 -)
12 +) &&
13 +
14 +{
15 + echo a ; ?!AMP?! echo b
16 +} &&
17 +{ echo a ; ?!AMP?! echo b ; } &&
18 +
19 +{
20 + echo "${var}9" &&
21 + echo "done"
22 +} &&
23 +finis
t/chainlint/block.test
+14 -1
@@ -11,4 +11,17 @@
11 echo c
12 }
13 baz
14 -)
14 +) &&
15 +
16 +# LINT: ";" not allowed in place of "&&"
17 +{
18 + echo a; echo b
19 +} &&
20 +{ echo a; echo b; } &&
21 +
22 +# LINT: "}" inside string not mistaken as end of block
23 +{
24 + echo "${var}9" &&
25 + echo "done"
26 +} &&
27 +finis
t/chainlint/chained-block.expect new
+9
@@ -0,0 +1,9 @@
1 +echo nobody home && {
2 + test the doohicky ?!AMP?!
3 + right now
4 +} &&
5 +
6 +GIT_EXTERNAL_DIFF=echo git diff | {
7 + read path oldfile oldhex oldmode newfile newhex newmode &&
8 + test "z$oh" = "z$oldhex"
9 +}
t/chainlint/chained-block.test new
+11
@@ -0,0 +1,11 @@
1 +# LINT: start of block chained to preceding command
2 +echo nobody home && {
3 + test the doohicky
4 + right now
5 +} &&
6 +
7 +# LINT: preceding command pipes to block on same line
8 +GIT_EXTERNAL_DIFF=echo git diff | {
9 + read path oldfile oldhex oldmode newfile newhex newmode &&
10 + test "z$oh" = "z$oldhex"
11 +}
t/chainlint/chained-subshell.expect new
+10
@@ -0,0 +1,10 @@
1 +mkdir sub && (
2 + cd sub &&
3 + foo the bar ?!AMP?!
4 + nuff said
5 +) &&
6 +
7 +cut "-d " -f actual | ( read s1 s2 s3 &&
8 +test -f $s1 ?!AMP?!
9 +test $(cat $s2) = tree2path1 &&
10 +test $(cat $s3) = tree3path1 )
t/chainlint/chained-subshell.test new
+13
@@ -0,0 +1,13 @@
1 +# LINT: start of subshell chained to preceding command
2 +mkdir sub && (
3 + cd sub &&
4 + foo the bar
5 + nuff said
6 +) &&
7 +
8 +# LINT: preceding command pipes to subshell on same line
9 +cut "-d " -f actual | (read s1 s2 s3 &&
10 +test -f $s1
11 +test $(cat $s2) = tree2path1 &&
12 +# LINT: closing subshell ")" correctly detected on same line as "$(...)"
13 +test $(cat $s3) = tree3path1)
t/chainlint/command-substitution-subsubshell.expect new
+2
@@ -0,0 +1,2 @@
1 +OUT=$(( ( large_git 1 >& 3 ) | : ) 3 >& 1) &&
2 +test_match_signal 13 "$OUT"
t/chainlint/command-substitution-subsubshell.test new
+3
@@ -0,0 +1,3 @@
1 +# LINT: subshell nested in subshell nested in command substitution
2 +OUT=$( ((large_git 1>&3) | :) 3>&1 ) &&
3 +test_match_signal 13 "$OUT"
t/chainlint/double-here-doc.expect new
+2
@@ -0,0 +1,2 @@
1 +run_sub_test_lib_test_err run-inv-range-start "--run invalid range start" --run="a-5" <<-EOF &&
2 +check_sub_test_lib_test_err run-inv-range-start <<-EOF_OUT 3 <<-EOF_ERR
t/chainlint/double-here-doc.test new
+12
@@ -0,0 +1,12 @@
1 +run_sub_test_lib_test_err run-inv-range-start \
2 + "--run invalid range start" \
3 + --run="a-5" <<-\EOF &&
4 +test_expect_success "passing test #1" "true"
5 +test_done
6 +EOF
7 +check_sub_test_lib_test_err run-inv-range-start \
8 + <<-\EOF_OUT 3<<-EOF_ERR
9 +> FATAL: Unexpected exit with code 1
10 +EOF_OUT
11 +> error: --run: invalid non-numeric in range start: ${SQ}a-5${SQ}
12 +EOF_ERR
t/chainlint/dqstring-line-splice.expect new
+3
@@ -0,0 +1,3 @@
1 +echo 'fatal: reword option of --fixup is mutually exclusive with' '--patch/--interactive/--all/--include/--only' > expect &&
2 +test_must_fail git commit --fixup=reword:HEAD~ $1 2 > actual &&
3 +test_cmp expect actual
t/chainlint/dqstring-line-splice.test new
+7
@@ -0,0 +1,7 @@
1 +# LINT: line-splice within DQ-string
2 +'"
3 +echo 'fatal: reword option of --fixup is mutually exclusive with'\
4 + '--patch/--interactive/--all/--include/--only' >expect &&
5 +test_must_fail git commit --fixup=reword:HEAD~ $1 2>actual &&
6 +test_cmp expect actual
7 +"'
t/chainlint/dqstring-no-interpolate.expect new
+11
@@ -0,0 +1,11 @@
1 +grep "^ ! [rejected][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out &&
2 +
3 +grep "^\.git$" output.txt &&
4 +
5 +
6 +(
7 + cd client$version &&
8 + GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. $(cat ../input)
9 +) > output &&
10 + cut -d ' ' -f 2 < output | sort > actual &&
11 + test_cmp expect actual
t/chainlint/dqstring-no-interpolate.test new
+15
@@ -0,0 +1,15 @@
1 +# LINT: regex dollar-sign eol anchor in double-quoted string not special
2 +grep "^ ! \[rejected\][ ]*$BRANCH -> $BRANCH (non-fast-forward)$" out &&
3 +
4 +# LINT: escaped "$" not mistaken for variable expansion
5 +grep "^\\.git\$" output.txt &&
6 +
7 +'"
8 +(
9 + cd client$version &&
10 +# LINT: escaped dollar-sign in double-quoted test body
11 + GIT_TEST_PROTOCOL_VERSION=$version git fetch-pack --no-progress .. \$(cat ../input)
12 +) >output &&
13 + cut -d ' ' -f 2 <output | sort >actual &&
14 + test_cmp expect actual
15 +"'
t/chainlint/empty-here-doc.expect new
+3
@@ -0,0 +1,3 @@
1 +git ls-tree $tree path > current &&
2 +cat > expected <<EOF &&
3 +test_output
t/chainlint/empty-here-doc.test new
+5
@@ -0,0 +1,5 @@
1 +git ls-tree $tree path >current &&
2 +# LINT: empty here-doc
3 +cat >expected <<\EOF &&
4 +EOF
5 +test_output
t/chainlint/exclamation.expect new
+4
@@ -0,0 +1,4 @@
1 +if ! condition ; then echo nope ; else yep ; fi &&
2 +test_prerequisite !MINGW &&
3 +mail uucp!address &&
4 +echo !whatever!
t/chainlint/exclamation.test new
+8
@@ -0,0 +1,8 @@
1 +# LINT: "! word" is two tokens
2 +if ! condition; then echo nope; else yep; fi &&
3 +# LINT: "!word" is single token, not two tokens "!" and "word"
4 +test_prerequisite !MINGW &&
5 +# LINT: "word!word" is single token, not three tokens "word", "!", and "word"
6 +mail uucp!address &&
7 +# LINT: "!word!" is single token, not three tokens "!", "word", and "!"
8 +echo !whatever!
t/chainlint/for-loop-abbreviated.expect new
+5
@@ -0,0 +1,5 @@
1 +for it
2 +do
3 + path=$(expr "$it" : ( [^:]*) ) &&
4 + git update-index --add "$path" || exit
5 +done
t/chainlint/for-loop-abbreviated.test new
+6
@@ -0,0 +1,6 @@
1 +# LINT: for-loop lacking optional "in [word...]" before "do"
2 +for it
3 +do
4 + path=$(expr "$it" : '\([^:]*\)') &&
5 + git update-index --add "$path" || exit
6 +done
t/chainlint/function.expect new
+11
@@ -0,0 +1,11 @@
1 +sha1_file ( ) {
2 + echo "$*" | sed "s#..#.git/objects/&/#"
3 +} &&
4 +
5 +remove_object ( ) {
6 + file=$(sha1_file "$*") &&
7 + test -e "$file" ?!AMP?!
8 + rm -f "$file"
9 +} ?!AMP?!
10 +
11 +sha1_file arg && remove_object arg
t/chainlint/function.test new
+13
@@ -0,0 +1,13 @@
1 +# LINT: "()" in function definition not mistaken for subshell
2 +sha1_file() {
3 + echo "$*" | sed "s#..#.git/objects/&/#"
4 +} &&
5 +
6 +# LINT: broken &&-chain in function and after function
7 +remove_object() {
8 + file=$(sha1_file "$*") &&
9 + test -e "$file"
10 + rm -f "$file"
11 +}
12 +
13 +sha1_file arg && remove_object arg
t/chainlint/here-doc-indent-operator.expect new
+5
@@ -0,0 +1,5 @@
1 +cat > expect <<-EOF &&
2 +
3 +cat > expect <<-EOF ?!AMP?!
4 +
5 +cleanup
t/chainlint/here-doc-indent-operator.test new
+13
@@ -0,0 +1,13 @@
1 +# LINT: whitespace between operator "<<-" and tag legal
2 +cat >expect <<- EOF &&
3 +header: 43475048 1 $(test_oid oid_version) $NUM_CHUNKS 0
4 +num_commits: $1
5 +chunks: oid_fanout oid_lookup commit_metadata generation_data bloom_indexes bloom_data
6 +EOF
7 +
8 +# LINT: not an indented here-doc; just a plain here-doc with tag named "-EOF"
9 +cat >expect << -EOF
10 +this is not indented
11 +-EOF
12 +
13 +cleanup
t/chainlint/if-condition-split.expect new
+7
@@ -0,0 +1,7 @@
1 +if bob &&
2 + marcia ||
3 + kevin
4 +then
5 + echo "nomads" ?!AMP?!
6 + echo "for sure"
7 +fi
t/chainlint/if-condition-split.test new
+8
@@ -0,0 +1,8 @@
1 +# LINT: "if" condition split across multiple lines at "&&" or "||"
2 +if bob &&
3 + marcia ||
4 + kevin
5 +then
6 + echo "nomads"
7 + echo "for sure"
8 +fi
t/chainlint/one-liner-for-loop.expect new
+9
@@ -0,0 +1,9 @@
1 +git init dir-rename-and-content &&
2 +(
3 + cd dir-rename-and-content &&
4 + test_write_lines 1 2 3 4 5 >foo &&
5 + mkdir olddir &&
6 + for i in a b c; do echo $i >olddir/$i; ?!LOOP?! done ?!AMP?!
7 + git add foo olddir &&
8 + git commit -m "original" &&
9 +)
t/chainlint/one-liner-for-loop.test new
+10
@@ -0,0 +1,10 @@
1 +git init dir-rename-and-content &&
2 +(
3 + cd dir-rename-and-content &&
4 + test_write_lines 1 2 3 4 5 >foo &&
5 + mkdir olddir &&
6 +# LINT: one-liner for-loop missing "|| exit"; also broken &&-chain
7 + for i in a b c; do echo $i >olddir/$i; done
8 + git add foo olddir &&
9 + git commit -m "original" &&
10 +)
t/chainlint/sqstring-in-sqstring.expect new
+4
@@ -0,0 +1,4 @@
1 +perl -e '
2 + defined($_ = -s $_) or die for @ARGV;
3 + exit 1 if $ARGV[0] <= $ARGV[1];
4 +' test-2-$packname_2.pack test-3-$packname_3.pack
t/chainlint/sqstring-in-sqstring.test new
+5
@@ -0,0 +1,5 @@
1 +# LINT: SQ-string Perl code fragment within SQ-string
2 +perl -e '\''
3 + defined($_ = -s $_) or die for @ARGV;
4 + exit 1 if $ARGV[0] <= $ARGV[1];
5 +'\'' test-2-$packname_2.pack test-3-$packname_3.pack
t/chainlint/token-pasting.expect new
+27
@@ -0,0 +1,27 @@
1 +git config filter.rot13.smudge ./rot13.sh &&
2 +git config filter.rot13.clean ./rot13.sh &&
3 +
4 +{
5 + echo "*.t filter=rot13" ?!AMP?!
6 + echo "*.i ident"
7 +} > .gitattributes &&
8 +
9 +{
10 + echo a b c d e f g h i j k l m ?!AMP?!
11 + echo n o p q r s t u v w x y z ?!AMP?!
12 + echo '$Id$'
13 +} > test &&
14 +cat test > test.t &&
15 +cat test > test.o &&
16 +cat test > test.i &&
17 +git add test test.t test.i &&
18 +rm -f test test.t test.i &&
19 +git checkout -- test test.t test.i &&
20 +
21 +echo "content-test2" > test2.o &&
22 +echo "content-test3 - filename with special characters" > "test3 'sq',$x=.o" ?!AMP?!
23 +
24 +downstream_url_for_sed=$(
25 + printf "%sn" "$downstream_url" |
26 + sed -e 's/\/\\/g' -e 's/[[/.*^$]/\&/g'
27 +)
t/chainlint/token-pasting.test new
+32
@@ -0,0 +1,32 @@
1 +# LINT: single token; composite of multiple strings
2 +git config filter.rot13.smudge ./rot13.sh &&
3 +git config filter.rot13.clean ./rot13.sh &&
4 +
5 +{
6 + echo "*.t filter=rot13"
7 + echo "*.i ident"
8 +} >.gitattributes &&
9 +
10 +{
11 + echo a b c d e f g h i j k l m
12 + echo n o p q r s t u v w x y z
13 +# LINT: exit/enter string context and escaped-quote outside of string
14 + echo '\''$Id$'\''
15 +} >test &&
16 +cat test >test.t &&
17 +cat test >test.o &&
18 +cat test >test.i &&
19 +git add test test.t test.i &&
20 +rm -f test test.t test.i &&
21 +git checkout -- test test.t test.i &&
22 +
23 +echo "content-test2" >test2.o &&
24 +# LINT: exit/enter string context and escaped-quote outside of string
25 +echo "content-test3 - filename with special characters" >"test3 '\''sq'\'',\$x=.o"
26 +
27 +# LINT: single token; composite of multiple strings
28 +downstream_url_for_sed=$(
29 + printf "%s\n" "$downstream_url" |
30 +# LINT: exit/enter string context; "&" inside string not command terminator
31 + sed -e '\''s/\\/\\\\/g'\'' -e '\''s/[[/.*^$]/\\&/g'\''
32 +)