summaryrefslogtreecommitdiff
path: root/t/chainlint.pl
AgeCommit message (Collapse)AuthorFilesLines
2026-07-06t: extract chainlint's parser into shared moduleMichael Montalbo1-517/+12
Move chainlint.pl's Lexer, ShellParser, and ScriptParser into a shared module (lib-shell-parser.pl) so other lint tools can reuse the same shell parsing infrastructure. A subsequent commit adds greplint.pl, which needs the same tokenizer to correctly identify command boundaries. ScriptParser's check_test() becomes a no-op in the shared module. chainlint.pl defines ChainlintParser (extending ScriptParser) with the &&-chain check_test() implementation. No functional change: chainlint produces the same output and check-chainlint self-tests pass. Signed-off-by: Michael Montalbo <mmontalbo@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-09-10chainlint: reduce annotation noise-factorEric Sunshine1-1/+2
When chainlint detects a problem in a test definition, it highlights the offending code with a "?!...?!" annotation. The rather curious "?!" decoration was chosen to draw the reader's attention to the problem area and to act as a good "needle" when using the terminal's search feature to "jump" to the next problem. Later, chainlint learned to color its output when sent to a terminal. Problem annotations are colored with a red background which stands out well from surrounding text, thus easily draws the reader's attention. Together with the preceding change which gave all problem annotations a uniform "LINT:" prefix, the noisy "?!" decoration has become superfluous as a search "needle" so omit it when output is colored. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-09-10chainlint: make error messages self-explanatoryEric Sunshine1-8/+22
The annotations emitted by chainlint to indicate detected problems are overly terse, so much so that developers new to the project -- those who should most benefit from the linting -- may find them baffling. For instance, although the author of chainlint and seasoned Git developers may understand that "?!AMP?!" is an abbreviation of "ampersand" and indicates a break in the &&-chain, this may not be obvious to newcomers. The "?!LOOP?!" case is particularly serious because that terse single word does nothing to convey that the loop body should end with "|| return 1" (or "|| exit 1" in a subshell) to ensure that a failing command in the body aborts the loop immediately. Moreover, unlike &&-chaining which is ubiquitous in Git tests, the "|| return 1" idiom is relatively infrequent, thus may be harder for a newcomer to discover by consulting nearby code. Address these shortcomings by emitting human-readable messages which both explain the problem and give a strong hint about how to correct it. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-09-10chainlint: don't be fooled by "?!...?!" in test bodyEric Sunshine1-5/+8
As originally implemented, chainlint did not collect structured information about detected problems. Instead, it merely emitted raw parse tokens (not the original test text), along with a "?!...?!" annotation directly into the output stream each time a problem was discovered. In order to report statistics (in --stats mode) and to adjust its exit code to indicate success or failure, it merely counts the number of times "?!...?!" appears in the output stream. An obvious shortcoming of this approach is that it can be fooled by a legitimate "?!...?!" sequence in the body of a test (though, only if an actual problem is detected in the test). The situation did not improve when 7c04aa7390 (chainlint: colorize problem annotations and test delimiters, 2022-09-13) colored the annotations after-the-fact by searching for "?!...?!" in the output stream and inserting color codes. As above, a shortcoming is that this approach can incorrectly color a legitimate "?!...?!" sequence in a test body as if it is an error. However, when 73c768dae9 (chainlint: annotate original test definition rather than token stream, 2022-11-08) taught chainlint to output the original test text verbatim, it started collecting structured information about detected problems. Now that it is available, take advantage of the structured problem information to deterministically count the number of problems detected and to color the annotations directly, rather than scanning the output stream for "?!...?!" and performing these operations after-the-fact. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-07-10chainlint.pl: recognize test bodies defined via heredocEric Sunshine1-5/+22
In order to check tests for semantic problems, chainlint.pl scans test scripts, looking for tests defined as: test_expect_success [prereq] title ' body ' where `body` is a single string which is then treated as a standalone chunk of code and "linted" to detect semantic issues. (The same happens for `test_expect_failure` definitions.) The introduction of test definitions in which the test body is instead presented via a heredoc rather than as a single string creates a blind spot in the linting process since such invocations are not recognized by chainlint.pl. Prepare for this new style by also recognizing tests defined as: test_expect_success [prereq] title - <<\EOT body EOT A minor complication is that chainlint.pl has never considered heredoc bodies significant since it doesn't scan them for semantic problems, thus it has always simply thrown them away. However, with the new `test_expect_success` calling sequence, heredoc bodies become meaningful, thus need to be captured. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-07-10chainlint.pl: force CRLF conversion when opening input filesJeff King1-1/+1
The lexer in chainlint.pl can't handle CRLF line endings; it complains about an internal error in scan_token() if we see one. For example, in our Windows CI environment: $ perl chainlint.pl chainlint/for-loop.test | cat -v Thread 2 terminated abnormally: internal error scanning character '^M' This doesn't break "make check-chainlint" (yet), because we assemble a concatenated input by passing the contents of each file through "sed". And the "sed" we use will strip out the CRLFs. But the next patch is going to rework this a bit, which does break check-chainlint on Windows. Plus it's probably nicer to folks on Windows who might work on chainlint itself and write new tests. In theory we could fix the parser to handle this, but it's not really worth the trouble. We should be able to ask the input layer to translate the line endings for us. In fact, I'd expect this to happen by default, as perl's documentation claims Win32 uses the ":unix:crlf" PERLIO layer by default ("unix" here just refers to using read/write syscalls, and then "crlf" layers the translation on top). However, this doesn't seem to be the case in our Windows CI environment. I didn't dig into the exact reason, but it is perhaps because we are using an msys build of perl rather than a "true" Win32 build. At any rate, it is easy-ish to just ask explicitly for the conversion. In the above example, setting PERLIO=crlf in the environment is enough to make it work. Curiously, though, this doesn't work when invoking chainlint via "make". Again, I didn't dig into it, but it may have to do with msys programs calling Windows programs or vice versa. We can make it work consistently by just explicitly asking for CRLF translation when we open the files. This will even work on non-Windows platforms, though we wouldn't really expect to find CRLF files there. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-07-10chainlint.pl: do not spawn more threads than we have scriptsJeff King1-0/+1
The chainlint.pl script spawns worker threads to check many scripts in parallel. This is good if you feed it a lot of scripts. But if you give it few (or one), then the overhead of spawning the threads dominates. We can easily notice that we have fewer scripts than threads and scale back as appropriate. This patch reduces the time to run: time for i in chainlint/*.test; do perl chainlint.pl $i done >/dev/null on my system from ~4.1s to ~1.1s, where I have 8+8 cores. As with the previous patch, this isn't the usual way we run chainlint (we feed many scripts at once, which is why it supports threading in the first place). So this won't make a big difference in the real world, but it may help us out in the future, and it makes experimenting with and debugging the chainlint tests a bit more pleasant. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-07-10chainlint.pl: only start threads if jobs > 1Jeff King1-1/+2
If the system supports threads, chainlint.pl will always spawn worker threads to do the real work. But when --jobs=1, this is pointless, since we could just do the work in the main thread. And spawning even a single thread has a high overhead. For example, on my Linux system, running: for i in chainlint/*.test; do perl chainlint.pl --jobs=1 $i done >/dev/null takes ~1.7s without this patch, and ~1.1s after. We don't usually spawn a bunch of individual chainlint.pl processes (instead we feed several scripts at once, and the parallelism outweighs the setup cost). But it's something we've considered doing, and since we already have fallback code for systems without thread support, it's pretty easy to make this work. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-05-30Merge branch 'es/chainlint-ncores-fix'Junio C Hamano1-3/+17
The chainlint script (invoked during "make test") did nothing when it failed to detect the number of available CPUs. It now falls back to 1 CPU to avoid the problem. * es/chainlint-ncores-fix: chainlint.pl: latch CPU count directly reported by /proc/cpuinfo chainlint.pl: fix incorrect CPU count on Linux SPARC chainlint.pl: make CPU count computation more robust
2024-05-22chainlint.pl: latch CPU count directly reported by /proc/cpuinfoEric Sunshine1-0/+3
On Linux, ncores() computes the number of CPUs by counting the "processor" or "CPU" lines emitted by /proc/cpuinfo. However, on some platforms, /proc/cpuinfo does not enumerate the CPUs at all, but instead merely mentions the total number of CPUs. In such cases, pluck the CPU count directly from the /proc/cpuinfo line which reports the number of active CPUs. (In particular, check for "cpus active: NN" and "ncpus active: NN" since both variants have been seen in the wild[1,2].) [1]: https://lore.kernel.org/git/503a99f3511559722a3eeef15d31027dfe617fa1.camel@physik.fu-berlin.de/ [2]: https://lore.kernel.org/git/7acbd5c6c68bd7ba020e2d1cc457a8954fd6edf4.camel@physik.fu-berlin.de/ Reported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-05-22chainlint.pl: fix incorrect CPU count on Linux SPARCJohn Paul Adrian Glaubitz1-1/+1
On SPARC systems running Linux, individual processors are denoted with "CPUnn:" in /proc/cpuinfo instead of the usual "processor : NN". As a result, the regexp in ncores() matches 0 times. Address this shortcoming by extending the regexp to also match lines with "CPUnn:". Signed-off-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> [es: simplified regexp; tweaked commit message] Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Tested-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2024-05-20chainlint.pl: make CPU count computation more robustEric Sunshine1-3/+14
There have been reports[1,2] of chainlint.pl failing to produce output when output is expected. In fact, the underlying problem is more severe: in these cases, it isn't doing any work at all, thus not checking Git tests for semantic problems. In the reported cases, the problem was tracked down to ncores() returning 0 for the CPU count, which resulted in chainlint.pl not performing any work (since it thought it had no cores on which to process). In the reported cases, the reason for the failure was that the regular expression counting the number of processors reported by /proc/cpuinfo failed to find any matches, hence it counted 0 processors. Although fixing each case as it is reported allows chaining.pl to work correctly on that architecture, it does nothing to improve the overall robustness of the core count computation which may still return 0 on some yet untested architecture. Address this shortcoming by ensuring that ncores() returns a sensible fallback value in all cases. [1]: https://lore.kernel.org/git/pull.1385.git.git.1669148861635.gitgitgadget@gmail.com/ [2]: https://lore.kernel.org/git/8baa12f8d044265f1ddeabd64209e7ac0d3700ae.camel@physik.fu-berlin.de/ Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-03-30tests: diagnose unclosed here-doc in chainlint.plEric Sunshine1-3/+12
An unclosed here-doc in a test is a problem, because it silently gobbles up any remaining commands. Since 99a64e4b73c (tests: lint for run-away here-doc, 2017-03-22) we detect this by piggy-backing on the internal chainlint checker in test-lib.sh. However, it would be nice to detect it in chainlint.pl, for a few reasons: - the output from chainlint.pl is much nicer; it can show the exact spot of the error, rather than a vague "somewhere in this test you broke the &&-chain or had a bad here-doc" message. - the implementation in test-lib.sh runs for each test snippet. And since it requires a subshell, the extra cost is small but not zero. If chainlint.pl can reliably find the problem, we can optimize the test-lib.sh code. The chainlint.pl code never intended to find here-doc problems. But since it has to parse them anyway (to avoid reporting problems inside here-docs), most of what we need is already there. We can detect the problem when we fail to find the missing end-tag in swallow_heredocs(). The extra change in scan_heredoc_tag() stores the location of the start of the here-doc, which lets us mark it as the source of the error in the output (see the new tests for examples). [jk: added commit message and tests] Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-11-29Merge branch 'ah/chainlint-cpuinfo-parse-fix'Junio C Hamano1-1/+1
The format of a line in /proc/cpuinfo that describes a CPU on s390x looked different from everybody else, and the code in chainlint.pl failed to parse it. * ah/chainlint-cpuinfo-parse-fix: chainlint.pl: fix /proc/cpuinfo regexp
2022-11-23chainlint.pl: fix /proc/cpuinfo regexpAndreas Hasenack1-1/+1
29fb2ec3 (chainlint.pl: validate test scripts in parallel, 2022-09-01) introduced a function that gets the number of cores from /proc/cpuinfo on some systems, notably linux. The regexp it uses (^processor\s*:) fails to match the desired lines in the s390x architecture, where they look like this: processor 0: version = FF, identification = 148F67, machine = 2964 As a result, on s390x that function returns 0 as the number of cores, and the chainlint.pl script exits without doing anything. Signed-off-by: Andreas Hasenack <andreas.hasenack@canonical.com> Acked-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-11-11chainlint: prefix annotated test definition with line numbersEric Sunshine1-2/+8
When chainlint detects problems in a test, it prints out the name of the test script, the name of the problematic test, and a copy of the test definition with "?!FOO?!" annotations inserted at the locations where problems were detected. Taken together this information is sufficient for the test author to identify the problematic code in the original test definition. However, in a lengthy script or a lengthy test definition, the author may still end up using the editor's search feature to home in on the exact problem location. To further assist the test author, display line numbers along with the annotated test definition, thus allowing the author to jump directly to each problematic line. Suggested-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-11chainlint: latch line numbers at which each token starts and endsEric Sunshine1-8/+17
When chainlint detects problems in a test, it prints out the name of the test script, the name of the problematic test, and a copy of the test definition with "?!FOO?!" annotations inserted at the locations where problems were detected. Taken together this information is sufficient for the test author to identify the problematic code in the original test definition. However, in a lengthy script or a lengthy test definition, the author may still end up using the editor's search feature to home in on the exact problem location. To further assist the test author, an upcoming change will display line numbers along with the annotated test definition, thus allowing the author to jump directly to each problematic line. As preparation, upgrade Lexer to latch the line numbers at which each token starts and ends, and return that information with the token itself. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-11chainlint: sidestep impoverished macOS "terminfo"Eric Sunshine1-12/+23
Although the macOS Terminal.app is "xterm"-compatible, its corresponding "terminfo" entries -- such as "xterm", "xterm-256color", and "xterm-new"[1] -- neglect to mention capabilities which Terminal.app actually supports (such as "dim text"). This oversight on Apple's part ends up penalizing users of "good citizen" console programs which consult "terminfo" to tailor their output based upon reported terminal capabilities (as opposed to programs which assume that the terminal supports ANSI codes). The same problem is present in other Apple "terminfo" entries, such as "nsterm"[2], with which macOS Terminal.app may be configured. Sidestep this Apple problem by imbuing get_colors() with specific knowledge of capabilities common to "xterm" and "nsterm", rather than trusting "terminfo" to report them correctly. Although hard-coding such knowledge is ugly, "xterm" support is nearly ubiquitous these days, and Git itself sets precedence by assuming support for ANSI color codes. For other terminal types, fall back to querying "terminfo" via `tput` as usual. FOOTNOTES [1] iTerm2 FAQ suggests "xterm-new": https://iterm2.com/faq.html [2] Neovim documentation recommends terminal type "nsterm" with Terminal.app: https://neovim.io/doc/user/term.html#terminfo Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-08chainlint: annotate original test definition rather than token streamEric Sunshine1-7/+24
When chainlint detects problems in a test, such as a broken &&-chain, it prints out the test with "?!FOO?!" annotations inserted at each problem location. However, rather than annotating the original test definition, it instead dumps out a parsed token representation of the test. Since it lacks comments, indentations, here-doc bodies, and so forth, this tokenized representation can be difficult for the test author to digest and relate back to the original test definition. However, now that each parsed token carries positional information, the location of a detected problem can be pinpointed precisely in the original test definition. Therefore, take advantage of this information to annotate the test definition itself rather than annotating the parsed token stream, thus making it easier for a test author to relate a problem back to the source. Maintaining the positional meta-information associated with each detected problem requires a slight change in how the problems are managed internally. In particular, shell syntax such as: msg="total: $(cd data; wc -w *.txt) words" requires the lexical analyzer to recursively invoke the parser in order to detect problems within the $(...) expression inside the double-quoted string. In this case, the recursive parse context will detect the broken &&-chain between the `cd` and `wc` commands, returning the token stream: cd data ; ?!AMP?! wc -w *.txt However, the parent parse context will see everything inside the double-quotes as a single string token: "total: $(cd data ; ?!AMP?! wc -w *.txt) words" losing whatever positional information was attached to the ";" token where the problem was detected. One way to preserve the positional information of a detected problem in a recursive parse context within a string would be to attach the positional information to the annotation textually; for instance: "total: $(cd data ; ?!AMP:21:22?! wc -w *.txt) words" and then extract the positional information when annotating the original test definition. However, a cleaner and much simpler approach is to maintain the list of detected problems separately rather than embedding the problems as annotations directly in the parsed token stream. Not only does this ensure that positional information within recursive parse contexts is not lost, but it keeps the token stream free from non-token pollution, which may simplify implementation of validations added in the future since they won't have to handle non-token "?!FOO!?" items specially. Finally, the chainlint self-test "expect" files need a few mechanical adjustments now that the original test definitions are emitted rather than the parsed token stream. In particular, the following items missing from the historic parsed-token output are now preserved verbatim: * indentation (and whitespace, in general) * comments * here-doc bodies * here-doc tag quoting (i.e. "\EOF") * line-splices (i.e. "\" at the end of a line) Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-08chainlint: latch start/end position of each tokenEric Sunshine1-37/+43
When chainlint detects problems in a test, such as a broken &&-chain, it prints out the test with "?!FOO?!" annotations inserted at each problem location. However, rather than annotating the original test definition, it instead dumps out a parsed token representation of the test. Since it lacks comments, indentations, here-doc bodies, and so forth, this tokenized representation can be difficult for the test author to digest and relate back to the original test definition. To address this shortcoming, an upcoming change will make it print out an annotated copy of the original test definition rather than the tokenized representation. In order to do so, it will need to know the start and end positions of each token in the original test definition. As preparation, upgrade TestParser::scan_token() to latch the start and end position of the token being scanned, and return that information along with the token itself. A subsequent change will take advantage of this positional information. In terms of implementation, TestParser::scan_token() is retrofitted to return a tuple consisting of the token's lexeme and its start and end positions, rather than returning just the lexeme. However, an alternative would be to define a class which represents a token: package Token; sub new { my ($class, $lexeme, $start, $end) = @_; bless [$lexeme, $start, $end] => $class; } sub as_string { my $self = shift @_; return $self->[0]; } sub compare { my ($x, $y) = @_; if (UNIVERSAL::isa($y, 'Token')) { return $x->[0] cmp $y->[0]; } return $x->[0] cmp $y; } use overload ( '""' => 'as_string', 'cmp' => 'compare' ); The major benefit of the class-based approach is that it is entirely non-invasive; it requires no additional changes to the rest of the script since a Token converts automatically to a string, which is what scan_token() historically returned. The big downside to the Token approach, however, is that it is _slow_; on this developer's (old) machine, it increases user-time by an unacceptable seven seconds when scanning all test scripts in the project. Hence, the simple tuple approach is employed instead since it adds only a fraction of a second user-time. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-08chainlint: tighten accuracy when consuming input streamEric Sunshine1-1/+1
To extract the next token in the input stream, Lexer::scan_token() finds the start of the token by skipping whitespace, then consumes characters belonging to the token until it encounters a non-token character, such as an operator, punctuation, or whitespace. In the case of an operator or punctuation which ends a token, before returning the just-scanned token, it pushes that operator or punctuation character back onto the input stream to ensure that it will be the first character consumed by the next call to scan_token(). However, scan_token() is intentionally lax when whitespace ends a token; it doesn't bother pushing the whitespace character back onto the token stream since it knows that the next call to scan_token() will, as its first step, skip over whitespace anyhow when looking for the start of the token. Although such laxity is harmless for the proper functioning of the lexical analyzer, it does make it difficult to precisely identify the token's end position in the input stream. Accurate token position information may be desirable, for instance, to annotate problems or highlight other interesting facets of the input found during the parsing phase. To accommodate such possibilities, tighten scan_token() by making it push the token-ending whitespace character back onto the input stream, just as it does for other token-ending characters. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-11-08chainlint: add explanatory commentsEric Sunshine1-0/+4
The logic in TestParser::accumulate() for detecting broken &&-chains is mostly well-commented, but a couple branches which were deemed obvious and straightforward lack comments. In retrospect, though, these cases may give future readers pause, so comment them, as well. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Taylor Blau <me@ttaylorr.com>
2022-09-12chainlint: colorize problem annotations and test delimitersEric Sunshine1-3/+43
When `chainlint.pl` detects problems in a test definition, it emits the test definition with "?!FOO?!" annotations highlighting the problems it discovered. For instance, given this problematic test: test_expect_success 'discombobulate frobnitz' ' git frob babble && (echo balderdash; echo gnabgib) >expect && for i in three two one do git nitfol $i done >actual test_cmp expect actual ' chainlint.pl will output: # chainlint: t1234-confusing.sh # chainlint: discombobulate frobnitz git frob babble && (echo balderdash ; ?!AMP?! echo gnabgib) >expect && for i in three two one do git nitfol $i ?!LOOP?! done >actual ?!AMP?! test_cmp expect actual in which it may be difficult to spot the "?!FOO?!" annotations. The problem is compounded when multiple tests, possibly in multiple scripts, fail "linting", in which case it may be difficult to spot the "# chainlint:" lines which delimit one problematic test from another. To ameliorate this potential problem, colorize the "?!FOO?!" annotations in order to quickly draw the test author's attention to the problem spots, and colorize the "# chainlint:" lines to help the author identify the name of each script and each problematic test. Colorization is disabled automatically if output is not directed to a terminal or if NO_COLOR environment variable is set. The implementation is specific to Unix (it employs `tput` if available) but works equally well in the Git for Windows development environment which emulates Unix sufficiently. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: allow `|| echo` to signal failure upstream of a pipeEric Sunshine1-0/+3
The use of `|| return` (or `|| exit`) to signal failure within a loop isn't effective when the loop is upstream of a pipe since the pipe swallows all upstream exit codes and returns only the exit code of the final command in the pipeline. To work around this limitation, tests may adopt an alternative strategy of signaling failure by emitting text which would never be emitted in the non-failing case. For instance: while condition do command1 && command2 || echo "impossible text" done | sort >actual && Such usage indicates deliberate thought about failure cases by the test author, thus flagging them as missing `|| return` (or `|| exit`) is not helpful. Therefore, take this case into consideration when checking for explicit loop termination. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: complain about loops lacking explicit failure handlingEric Sunshine1-0/+11
Shell `for` and `while` loops do not terminate automatically just because a command fails within the loop body. Instead, the loop continues to iterate and eventually returns the exit status of the final command of the final iteration, which may not be the command which failed, thus it is possible for failures to go undetected. Consequently, it is important for test authors to explicitly handle failure within the loop body by terminating the loop manually upon failure. This can be done by returning a non-zero exit code from within the loop body (i.e. `|| return 1`) or exiting (i.e. `|| exit 1`) if the loop is within a subshell, or by manually checking `$?` and taking some appropriate action. Therefore, add logic to detect and complain about loops which lack explicit `return` or `exit`, or `$?` check. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: don't flag broken &&-chain if failure indicated explicitlyEric Sunshine1-0/+8
There are quite a few tests which print an error messages and then explicitly signal failure with `false`, `return 1`, or `exit 1` as the final command in an `if` branch. In these cases, the tests don't bother maintaining the &&-chain between `echo` and the explicit "test failed" indicator. Since such constructs are manually signaling failure, their &&-chain breakage is legitimate and safe -- both for the command immediately preceding `false`, `return`, or `exit`, as well as for all preceding commands in the `if` branch. Therefore, stop flagging &&-chain breakage in these sorts of cases. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: don't flag broken &&-chain if `$?` handled explicitlyEric Sunshine1-0/+6
There are cases in which tests capture and check a command's exit code explicitly without employing test_expect_code(). They do so by intentionally breaking the &&-chain since it would be impossible to capture "$?" in the failing case if the `status=$?` assignment was part of the &&-chain. Since such constructs are manually checking the exit code, their &&-chain breakage is legitimate and safe, thus should not be flagged. Therefore, stop flagging &&-chain breakage in such cases. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: don't require `&` background command to end with `&&`Eric Sunshine1-1/+1
The exit status of the `&` asynchronous operator which starts a command in the background is unconditionally zero, and the few places in the test scripts which launch commands asynchronously are not interested in the exit status of the `&` operator (though they often capture the background command's PID). As such, there is little value in complaining about broken &&-chain for a command launched in the background, and doing so would only make busy-work for test authors. Therefore, take this special case into account when checking for &&-chain breakage. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: don't require `return|exit|continue` to end with `&&`Eric Sunshine1-2/+18
In order to check for &&-chain breakage, each time TestParser encounters a new command, it checks whether the previous command ends with `&&`, and -- with a couple exceptions -- signals breakage if it does not. The first exception is that a command may validly end with `||`, which is commonly employed as `command || return 1` at the very end of a loop body to terminate the loop early. The second is that piping one command's output with `|` to another command does not constitute a &&-chain break (the exit status of the pipe is the exit status of the final command in the pipe). However, it turns out that there are a few additional cases found in the wild in which it is likely safe for `&&` to be missing even when other commands follow. For instance: while {condition-1} do test {condition-2} || return 1 # or `exit 1` within a subshell more-commands done while {condition-1} do test {condition-2} || continue more-commands done Such cases indicate deliberate thought about failure modes by the test author, thus flagging them as breaking the &&-chain is not helpful. Therefore, take these special cases into consideration when checking for &&-chain breakage. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: validate test scripts in parallelEric Sunshine1-1/+49
Although chainlint.pl has undergone a good deal of optimization during its development -- increasing in speed significantly -- parsing and validating 1050+ scripts and 16500+ tests via Perl is not exactly instantaneous. However, perceived performance can be improved by taking advantage of the fact that there is no interdependence between test scripts or test definitions, thus parsing and validating can be done in parallel. The number of available cores is determined automatically but can be overridden via the --jobs option. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: add parser to identify test definitionsEric Sunshine1-3/+60
Finish fleshing out chainlint.pl by adding ScriptParser, a parser which scans shell scripts for tests defined by test_expect_success() and test_expect_failure(), plucks the test body from each definition, and passes it to TestParser for validation. It recognizes test definitions not only at the top-level of test scripts but also tests synthesized within compound commands such as loops and function. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: add parser to validate testsEric Sunshine1-0/+46
Continue fleshing out chainlint.pl by adding TestParser, a parser with special knowledge about how Git tests should be written; for instance, it knows that commands within a test body should be chained together with `&&`. An upcoming parser which plucks test definitions from test scripts will invoke TestParser for each test body it encounters. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: add POSIX shell parserEric Sunshine1-0/+243
Continue fleshing out chainlint.pl by adding a general purpose recursive descent parser for the POSIX shell command language. Although never invoked directly, upcoming parser subclasses will extend its functionality for specific purposes, such as plucking test definitions from input scripts and applying domain-specific knowledge to perform test validation. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01chainlint.pl: add POSIX shell lexical analyzerEric Sunshine1-0/+177
Begin fleshing out chainlint.pl by adding a lexical analyzer for the POSIX shell command language. The sole entry point Lexer::scan_token() returns the next token from the input. It will be called by the upcoming shell language parser. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
2022-09-01t: add skeleton chainlint.plEric Sunshine1-0/+115
Although chainlint.sed usefully identifies broken &&-chains in tests, it has several shortcomings which include: * only detects &&-chain breakage in subshells (one-level deep) * does not check for broken top-level &&-chains; that task is left to the "magic exit code 117" checker built into test-lib.sh, however, that detection does not extend to `{...}` blocks, `$(...)` expressions, or compound statements such as `if...fi`, `while...done`, `case...esac` * uses heuristics, which makes it (potentially) fallible and difficult to tweak to handle additional real-world cases * written in `sed` and employs advanced `sed` operators which are probably not well-known to many programmers, thus the pool of people who can maintain it is likely small * manually simulates recursion into subshells which makes it much more difficult to reason about than, say, a traditional top-down parser * checks each test as the test is run, which can get expensive for tests which are run repeatedly by functions or loops since their bodies will be checked over and over (tens or hundreds of times) unnecessarily To address these shortcomings, begin implementing a more functional and precise test linter which understands shell syntax and semantics rather than employing heuristics, thus is able to recognize structural problems with tests beyond broken &&-chains. The new linter is written in Perl, thus should be more accessible to a wider audience, and is structured as a traditional top-down parser which makes it much easier to reason about, and allows it to inspect compound statements within test bodies to any depth. Furthermore, it can check all test definitions in the entire project in a single invocation rather than having to be invoked once per test, and each test definition is checked only once no matter how many times the test is actually run. At this stage, the new linter is just a skeleton containing boilerplate which handles command-line options, collects and reports statistics, and feeds its arguments -- paths of test scripts -- to a (presently) do-nothing script parser for validation. Subsequent changes will flesh out the functionality. Signed-off-by: Eric Sunshine <sunshine@sunshineco.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>