This is an archive of the discontinued LLVM Phabricator instance.

[lit] Handle plain negations directly in the internal shell
ClosedPublic

Authored by mstorsjo on Mar 18 2021, 5:23 AM.

Details

Summary

Keep running "not --crash" via the external "not" executable, but
for plain negations, and for cases that use the shell "!" operator,
just skip that argument and invert the return code.

The libcxx tests only use the shell operator "!" for negations,
never the "not" executable, because libcxx tests can be run without
having a fully built llvm tree available providing the "not"
executable.

This allows using the internal shell for libcxx tests.

Diff Detail

Event Timeline

mstorsjo created this revision.Mar 18 2021, 5:23 AM
mstorsjo requested review of this revision.Mar 18 2021, 5:23 AM
Herald added a project: Restricted Project. · View Herald TranscriptMar 18 2021, 5:24 AM
rnk accepted this revision.Mar 18 2021, 2:22 PM

lgtm

This revision is now accepted and ready to land.Mar 18 2021, 2:22 PM

FWIW, I'm looking into a test failure on one buildbot relating to this, https://lab.llvm.org/buildbot/#/builders/123/builds/3464/steps/8/logs/stdio, where clang-tidy/infrastructure/validate-check-names.cpp now fails.

jdenny reopened this revision.Mar 23 2021, 6:42 AM

Please add tests before committing this again.

This revision is now accepted and ready to land.Mar 23 2021, 6:42 AM
jdenny requested changes to this revision.Mar 23 2021, 6:43 AM
This revision now requires changes to proceed.Mar 23 2021, 6:43 AM
mstorsjo updated this revision to Diff 333503.Mar 26 2021, 12:15 AM

@jdenny - Updated with some tests. The existing tests in the shtest-not suite seem to cover the uses of 'not' quite well - so just for the change of making 'not' evaluated builtin those existing tests should cover all aspects I think. But for additionally handling '!' as equivalent to 'not' (except for the --crash option), I added a couple simple tests in the shtest-not suite - I didn't choose to duplicate all the aspects of suite but only a few trivial ones.

To actually be able to reland this, we'd need to figure out a sensible way to handle the unrelated breakage that just surfaces when this is in, e.g. with D99330 or something else similar.

@jdenny - Updated with some tests. The existing tests in the shtest-not suite seem to cover the uses of 'not' quite well - so just for the change of making 'not' evaluated builtin those existing tests should cover all aspects I think.

But lit's test suite doesn't check that internal not is actually used instead of external not, right? Is that change more a side effect than a goal of this patch?

If it seems worthwhile to verify that internal not continues to be used, we do have llvm/utils/lit/tests/Inputs/fake-externals, which is a directory of fake external commands that always fail. It's in PATH while running lit's own test suite to prove that external commands aren't called accidentally. You could add a simple not script there. The only challenge I foresee is that it would need to defer to the external not if --crash is the first argument.

But for additionally handling '!' as equivalent to 'not' (except for the --crash option), I added a couple simple tests in the shtest-not suite - I didn't choose to duplicate all the aspects of suite but only a few trivial ones.

Thanks for adding those.

I agree you don't need to replicate all existing not tests for !, but I feel there are a few more basic cases that ought to be covered. Most importantly, your new tests would pass even if ! always blindly has exit status zero. not-calls-diff.txt (at least) checks that not produces a non-zero exit status when expected. Also, there's no test that ! properly complains about a missing subcommand. That's a separate code path from when not doesn't have a subcommand.

To actually be able to reland this, we'd need to figure out a sensible way to handle the unrelated breakage that just surfaces when this is in, e.g. with D99330 or something else similar.

Understood. If possible, I'd like to leave that review to people who work regularly in windows.

But lit's test suite doesn't check that internal not is actually used instead of external not, right? Is that change more a side effect than a goal of this patch?

The main target is to make '!' work for negations when using the internal shell, as there's no '!' executable. (Libcxx uses '!' as it can be tested without building llvm executables.) Technically it would be enough to only make '!' a builtin and keep 'not' external - but that might be a bigger mess implementation wise. (That would sidestep the whole windows arg quoting can of worms that I opened though.)

If it seems worthwhile to verify that internal not continues to be used, we do have llvm/utils/lit/tests/Inputs/fake-externals, which is a directory of fake external commands that always fail. It's in PATH while running lit's own test suite to prove that external commands aren't called accidentally. You could add a simple not script there.

Ah, good point, I can try to add that.

The only challenge I foresee is that it would need to defer to the external not if --crash is the first argument.

Hmm, maybe if one updates $PATH to remove the path containing $0 and then executes not again...

But for additionally handling '!' as equivalent to 'not' (except for the --crash option), I added a couple simple tests in the shtest-not suite - I didn't choose to duplicate all the aspects of suite but only a few trivial ones.

Thanks for adding those.

I agree you don't need to replicate all existing not tests for !, but I feel there are a few more basic cases that ought to be covered. Most importantly, your new tests would pass even if ! always blindly has exit status zero. not-calls-diff.txt (at least) checks that not produces a non-zero exit status when expected. Also, there's no test that ! properly complains about a missing subcommand. That's a separate code path from when not doesn't have a subcommand.

Thanks, those are good points. Will add more tests for those cases.

To actually be able to reland this, we'd need to figure out a sensible way to handle the unrelated breakage that just surfaces when this is in, e.g. with D99330 or something else similar.

Understood. If possible, I'd like to leave that review to people who work regularly in windows.

Yeah, definitely :-) It's quite a mess...

mstorsjo updated this revision to Diff 334531.Mar 31 2021, 2:10 PM

Improved the testcases for '!'.

Adding a fake external for the 'not' executable is a bit tricky though... With the code as is, for a sequence of 'not not --crash <cmd>', it deduces that there's a '--crash' option in there, and chooses not to switch to the internal not for the outer invocation either.

To fix that case, I'd have to change the code to peel off as many 'not' invocations from the top until the first one with a '--crash' option. Not sure if that's worth the extra complexity for the implementation - what do you think?

Adding a fake external for the 'not' executable is a bit tricky though... With the code as is, for a sequence of 'not not --crash <cmd>', it deduces that there's a '--crash' option in there, and chooses not to switch to the internal not for the outer invocation either.

To fix that case, I'd have to change the code to peel off as many 'not' invocations from the top until the first one with a '--crash' option. Not sure if that's worth the extra complexity for the implementation - what do you think?

@jdenny - with this change to the functionality, I could add a fake-external not to to the tests, to make sure we always evaluate all the leading instances of not/! internally, up to the first not --crash:

diff --git a/llvm/utils/lit/lit/TestRunner.py b/llvm/utils/lit/lit/TestRunner.py
index fbb8e93058a2..8e07d09e8bd6 100644
--- a/llvm/utils/lit/lit/TestRunner.py
+++ b/llvm/utils/lit/lit/TestRunner.py
@@ -708,16 +708,18 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
                                                 " subcommand")
             elif args[0] == 'not':
                 not_args.append(args.pop(0))
-                not_count += 1
                 if args and args[0] == '--crash':
                     not_args.append(args.pop(0))
                     not_crash = True
+                if not not_crash:
+                    not_count += 1
                 if not args:
                     raise InternalShellError(j, "Error: 'not' requires a"
                                                 " subcommand")
             elif args[0] == '!':
                 not_args.append(args.pop(0))
-                not_count += 1
+                if not not_crash:
+                    not_count += 1
                 if not args:
                     raise InternalShellError(j, "Error: '!' requires a"
                                                 " subcommand")
@@ -769,13 +771,10 @@ def _executeShCmd(cmd, shenv, results, timeoutHelper):
         # blindly pass along the status it receives from any command it calls.
         
         # For plain negations, either 'not' without '--crash', or the shell
-        # operator '!', leave them out from the command to execute and
-        # invert the result code afterwards.
-        if not_crash:
-            args = not_args + args
-            not_count = 0                       
-        else:
-            not_args = []
+        # operator '!' (counted via not_count), leave them out from the
+        # command to execute and invert the result code afterwards.
+        args = not_args[not_count:] + args
+        not_args = []
        
         stdin, stdout, stderr = processRedirects(j, default_stdin, cmd_shenv,
                                                  opened_files)
jdenny accepted this revision.Apr 5 2021, 9:34 AM

Improved the testcases for '!'.

Thanks. LGTM except of course the windows problem you're addressing elsewhere.

Adding a fake external for the 'not' executable is a bit tricky though... With the code as is, for a sequence of 'not not --crash <cmd>', it deduces that there's a '--crash' option in there, and chooses not to switch to the internal not for the outer invocation either.

To fix that case, I'd have to change the code to peel off as many 'not' invocations from the top until the first one with a '--crash' option. Not sure if that's worth the extra complexity for the implementation - what do you think?

@jdenny - with this change to the functionality, I could add a fake-external not to to the tests, to make sure we always evaluate all the leading instances of not/! internally, up to the first not --crash:

With the change you proposed, what happens to not --crash not cmd? Won't the inner not be caught as a fake external? I suppose the not --crash would fail either way, but stderr would be different, and it would be more confusing to debug.

It seems to me this is getting too confusing. Sorry, my fake external suggestion is probably not worth the effort right now. Thanks for looking into it.

If you agree with that assessment, would you extend the fake-externals comments in llvm/utils/lit/tests/lit.cfg to mention why not isn't included? You might say: Don't do this for 'not' because lit uses the external 'not' throughout a RUN line that calls 'not --crash'.

Eventually, it would be great to see all this handling simplified. It seems one way to do that is to implement not --crash in python. But that's a different patch.

This revision is now accepted and ready to land.Apr 5 2021, 9:34 AM

With the change you proposed, what happens to not --crash not cmd? Won't the inner not be caught as a fake external? I suppose the not --crash would fail either way, but stderr would be different, and it would be more confusing to debug.

No, that case would work. My fake not executable looks like this:

#!/usr/bin/env python
 
import fake_external
import os
import subprocess
import sys
 
 
if len(sys.argv) > 1 and sys.argv[1] == '--crash':
  # Call the real 'not' binary; remove the dirname of argv[0] from the PATH
  paths = os.environ['PATH'].split(os.pathsep)
  cur_dir = os.path.dirname(sys.argv[0])
  if cur_dir in paths:
    paths.remove(cur_dir)
  os.environ['PATH'] = os.pathsep.join(paths)
  # Call the other 'not', replace argv[0] with a 'not' without a potential
  # path, to let it search for it in the path.
  args = sys.argv
  args[0] = 'not'
  sys.exit(subprocess.call(args))
 
fake_external.execute(__file__)

So the fake-externals dir gets removed from the path when re-executing not --crash.

It seems to me this is getting too confusing. Sorry, my fake external suggestion is probably not worth the effort right now. Thanks for looking into it.

If you agree with that assessment, would you extend the fake-externals comments in llvm/utils/lit/tests/lit.cfg to mention why not isn't included? You might say: Don't do this for 'not' because lit uses the external 'not' throughout a RUN line that calls 'not --crash'.

Sure, that sounds good to me.

Eventually, it would be great to see all this handling simplified. It seems one way to do that is to implement not --crash in python. But that's a different patch.

Yup, that sounds like a more worthwhile future direction.

Ah, I see. Even though it wouldn't add very much code, it sounds like we agree that the conceptual complexity isn't worthwhile given the direction we'd like to go eventually. Thanks again for looking into it.

mstorsjo updated this revision to Diff 335311.Apr 5 2021, 12:58 PM

Added the comment about why we don't have a fake external 'not'; leaving this review in approved state until we figure out a way forward with either D99330 or D99406.

FWIW, it should be possible to reapply this one now after D99938 landed, but I'll let the dust settle after that one first, before proceeding with this one.

hubert.reinterpretcast added inline comments.
llvm/utils/lit/lit/TestRunner.py
787

Hello, this logic is not actually sound: There are tests that are sensitive to the specific return code and not just whether the result is zero or not.

From http://lab.llvm.org:8014/#/builders/126/builds/273/steps/5/logs/stdio:

/scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/shtest-output-printing.py:15:15: error: CHECK-NEXT: expected string not found in input
# CHECK-NEXT: Exit Code: 1
              ^
/scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/Output/shtest-output-printing.py.tmp.out:9:3: note: scanning from here
--
  ^
/scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/Output/shtest-output-printing.py.tmp.out:10:1: note: possible intended match here
Exit Code: 2
^

Input file: /scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/Output/shtest-output-printing.py.tmp.out
Check file: /scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/shtest-output-printing.py

-dump-input=help explains the following input dump.

Input was:
<<<<<<
           1: -- Testing: 1 tests, 1 workers --_
           2: FAIL: shtest-output-printing :: basic.txt (1 of 1)_
           3: ******************** TEST 'shtest-output-printing :: basic.txt' FAILED ********************_
           4: Script:_
           5: --_
           6: : 'RUN: at line 1'; true_
           7: : 'RUN: at line 2'; echo hi_
           8: : 'RUN: at line 3'; not not wc missing-file &> /scratch/powerllvm/powerllvm_env/aix-ppc64-ppc64le/clang-ppc64-aix-ppc64le/stage1/utils/lit/tests/Inputs/shtest-output-printing/Output/basic.txt.tmp.out_
           9: --_
next:15'0       X error: no match found
          10: Exit Code: 2_
next:15'0     ~~~~~~~~~~~~~
next:15'1     ?             possible intended match

@mstorsjo, please revert if a fix (with an appropriate test) is not forthcoming.

mstorsjo added inline comments.Apr 18 2021, 2:39 PM
llvm/utils/lit/lit/TestRunner.py
787

Thanks for pointing this out, and sorry for the breakage. I'll push a fix including a test for it momentarily.

mstorsjo added inline comments.Apr 18 2021, 2:42 PM
llvm/utils/lit/lit/TestRunner.py
787

@jdenny I pushed a fix for this in rGd0b03ec401e8465b88893a4c56aeb0c787a54ad9, I hope you don't mind - let me know if you'd want changes to it.

llvm/utils/lit/lit/TestRunner.py
787

Thanks @mstorsjo for the quick fix! I'm seeing the test pass on AIX now.