This is an archive of the discontinued LLVM Phabricator instance.

test infra: catch bad decorators and import-time errors
ClosedPublic

Authored by tfiala on May 11 2016, 6:03 PM.

Details

Summary

This change enhances the LLDB test infrastructure to convert
load-time exceptions in a given Python test module into errors.
Before this change, specifying a non-existent test decorator,
or otherwise having some load-time error in a python test module,
would not get flagged as an error.

With this change, typos and other load-time errors in a python
test file get converted to errors and reported by the
test runner.

This change also includes test infrastructure tests that include
covering the new work here. I'm going to wait until we have
these infrastructure tests runnable on the main platforms before
I try to work that into all the normal testing workflows.

The test infrastructure tests can be run by using the standard python module testing practice of doing the following:

cd packages/Python/lldbsuite/test_event
python -m unittest discover -s test/src -p 'Test*.py'

Those tests run the dotest inferior with a known broken test and verify that the errors are caught. These tests did not pass until I modified dotest.py to capture them properly.

@zturner, if you have the chance, if you could try those steps above (the python -m unittest ... line) on Windows, that would be great if we can address any python2/3/Windows bits there. I don't think there's anything fancy, but I didn't want to hook it into test flow until I know it works there.

I'll be slowly adding more tests that cover some of the other breakage I've occasionally seen that didn't get collected as part of the summarization. This is the biggest one I'm aware of.

Diff Detail

Repository
rL LLVM

Event Timeline

tfiala updated this revision to Diff 56990.May 11 2016, 6:03 PM
tfiala retitled this revision from to test infra: catch bad decorators and import errors.
tfiala updated this object.
tfiala added reviewers: labath, zturner.
tfiala retitled this revision from test infra: catch bad decorators and import errors to test infra: catch bad decorators and import-time errors.
tfiala added subscribers: lldb-commits, zturner.
labath edited edge metadata.May 12 2016, 2:39 AM

I'm glad to see this getting fixed. I have a couple comments though. :)

packages/Python/lldbsuite/test/dotest.py
695 ↗(On Diff #56990)

This function (visit) is getting quite big, and due to the way it's structured, you were forced to duplicate some code. We could solve both problems with a bit of refactoring.

How about this:

  • put everything inside the for loop below this line into a separate function (visit_file ?)
  • put the try-catch block around the call to visit_file in this function

This way we will have less duplication and will reduce the nesting depth of this function.

What do you think?

725 ↗(On Diff #56990)

Will this ever be executed? I mean, if we get a pyc file, we shouldn't even get here due to the check on line 689.

packages/Python/lldbsuite/test_event/formatter/pickled.py
57 ↗(On Diff #56990)

Why do we need the format to be different based on the kind of object we are writing to? The added magic (introspection) seems like a bad tradeoff, if all it does is avoid a couple of lines in event_collector.py

packages/Python/lldbsuite/test_event/test/src/TestCatchInvalidDecorator.py
38 ↗(On Diff #56990)

I don't fully understand the whole event collector logic yet, but I would expect to see a check that the returned result is in fact an error result, no?

(Also, I prefer using assertGreater(foo, bar), as it provides a more descriptive error message.)

packages/Python/lldbsuite/test_event/test/src/event_collector.py
25 ↗(On Diff #56990)

I'm pretty sure this won't work on windows.. :)
tempfile.NamedTemporaryFile looks like a portable alternative.

I'll make some adjustments per your review, Pavel. Thanks! I'll put up another patch set at that point.

packages/Python/lldbsuite/test/dotest.py
695 ↗(On Diff #56990)

Sounds like a good change.

725 ↗(On Diff #56990)

Ah yes, that is true. Since we assert downstream from here if it was a .pyc file, I was being safe.

This will get refactored per previous comment, and at that point I will need it.

packages/Python/lldbsuite/test_event/formatter/pickled.py
57 ↗(On Diff #56990)

When we're using sockets, we have to be able to know the size of the full info when reconstructing on the receiving side. This is the normal mode in which this is used. However, that also complicates the parsing of the data for the simple test driver.

The code later on in the test_event unit tests:

if os.path.exists(pickled_events_filename()):
    with open(pickled_events_filename(), "rb") as events_file:
        while True:
            try:
                # print("reading event")
                event = cPickle.load(events_file)
                # print("read event: {}".format(event))
                if event:
                    events.append(event)
            except EOFError:
                # This is okay.
                Break

Would get considerably more complicated to deal with the same format that is only required for going over a network-style protocol. I prefer this tradeoff. In the unit tests, I just send the event output to a file, and then read it with the simple loop I included above.

However, to verify that I really prefer it, I will try writing it the other way.

packages/Python/lldbsuite/test_event/test/src/TestCatchInvalidDecorator.py
38 ↗(On Diff #56990)

.. but I would expect to see a check that the returned result is in fact an error result, no?

Ah yes. That is missing. (Before, we weren't getting any job_result or test_result events).

I'll add that.

(Also, I prefer using assertGreater(foo, bar), as it provides a more descriptive error message.)

Haha okay so I looked for that method when I originally worked this. The Python 2.7 page has a link to take to the assertion methods in a table. The table it points to doesn't have that call. However, they helpfully have two or three more tables below that. Not sure why all those calls don't go into the same table, particularly on a link for assertion methods from earlier in the page.

I totally agree, I'll change that.

packages/Python/lldbsuite/test_event/test/src/event_collector.py
25 ↗(On Diff #56990)

Oh yes. That's right. I was thinking I'd get around to trying this on Windows myself, but that didn't happen. At that point I was going to crack out the docs on the temp file handling.

Thanks for catching.

tfiala added inline comments.May 12 2016, 8:05 AM
packages/Python/lldbsuite/test_event/formatter/pickled.py
57 ↗(On Diff #56990)

However, to verify that I really prefer it, I will try writing it the other way.

The flip side is I can try to factor out the listener side logic that reconstructs these. However, that is currently rather tightly coupled to the network listening transport. And most of the work it does has purely to do with needing to receive the whole message before it can try to un-pickle it. So I'm not really seeing that as a big win. (Except for testability. So maybe it's okay to break that out.) I'll see what that looks like since that is probably the better high-level way to handle this if we didn't want the change I made to the sender side.

zturner edited edge metadata.May 12 2016, 8:08 AM

I will look at this in more detail later, but hold off on committing until
i have a chance to run it on Windows

I will look at this in more detail later, but hold off on committing until
i have a chance to run it on Windows

Hi Zachary.

No worries - I need to update the patch. Pavel caught one thing in the test for the new code that won't work on Windows that I forgot to fix up in the initial implementation. You might just as well wait for the next patch set to review it.

labath added inline comments.May 12 2016, 8:53 AM
packages/Python/lldbsuite/test_event/formatter/pickled.py
57 ↗(On Diff #56990)

The other alternative I see is to make pass in a "serializer" object (or a lambda or something), which knows how to write to the right output. Then, you can construct the correct serializer object depending on whether you got passed --results-file or --results-port.

tfiala updated this revision to Diff 57084.May 12 2016, 12:14 PM
tfiala edited edge metadata.

Address most of Pavel's review comments.

New:

  • Also catches multiple matching base names and converts that to a caught test error.

Comments from Pavel not covered:

  • Modifying the dual behavior of the pickling results formatter.

I'm going to look at that one next.

tfiala updated this revision to Diff 57087.May 12 2016, 12:16 PM

Adjust last patch to include full context.

tfiala marked 4 inline comments as done.May 12 2016, 12:18 PM
tfiala added inline comments.
packages/Python/lldbsuite/test/dotest.py
725 ↗(On Diff #56990)

You were right, once I refactored I saw I didn't need this check. I pulled it out.

packages/Python/lldbsuite/test_event/formatter/pickled.py
57 ↗(On Diff #56990)

Still outstanding.

tfiala updated this revision to Diff 57090.May 12 2016, 12:54 PM

Now addresses the remaining review comment from Pavel. ResultsFormatter instances are now told if their file is a stream rather than a block/char file.

The pickling formatter uses two different serializers, one for streaming and one for not.

tfiala marked an inline comment as done.May 12 2016, 2:10 PM

@zturner I think this is ready to go now. Can you give this a try on Windows?

Also, can you try the following part in cmd.exe?

cd packages\Python\lldbsuite\test_event
python -m unittest discover -s test\src -p 'Test*.py'

That should run 2 tests and they should pass, assuming no extra bits need to be added to the dotest.py that is called internally by them. The event_collector.py class, used to test the test infrastructure, builds a 'python dotest.py' command line that does what a dotest inferior will do during a normal test run. The output of that gets verified in the TestCatchInvalidDecorator.py test.

I had to leave early today, can I check it tomorrow?

I had to leave early today, can I check it tomorrow?

Sure, sounds good.

labath accepted this revision.May 13 2016, 1:40 AM
labath edited edge metadata.

Looks great from my side. Thank you for fixing this.

This revision is now accepted and ready to land.May 13 2016, 1:40 AM

Thanks, Pavel!

Going to look at this today, but I'm getting some crashes when I run the
lldb test suite, so I need to figure these out first so I have a clean
baseline for comparison.

Going to look at this today, but I'm getting some crashes when I run the
lldb test suite, so I need to figure these out first so I have a clean
baseline for comparison.

Okay.

No issues on Windows, looks good over here.

Do I still need to try the python -m unittest line? I didn't do that yet.

D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event>c:\Python35\python.exe
-m unittest discover -s test/src -p "Test*.py"
Traceback (most recent call last):

File "c:\Python35\lib\runpy.py", line 170, in _run_module_as_main
  "__main__", mod_spec)
File "c:\Python35\lib\runpy.py", line 85, in _run_code
  exec(code, run_globals)
File "c:\Python35\lib\unittest\__main__.py", line 18, in <module>
  main(module=None)
File "c:\Python35\lib\unittest\main.py", line 93, in __init__
  self.parseArgs(argv)
File "c:\Python35\lib\unittest\main.py", line 117, in parseArgs
  self._do_discovery(argv[2:])
File "c:\Python35\lib\unittest\main.py", line 228, in _do_discovery
  self.test = loader.discover(self.start, self.pattern, self.top)
File "c:\Python35\lib\unittest\loader.py", line 338, in discover
  raise ImportError('Start directory is not importable: %r' % start_dir)

ImportError: Start directory is not importable: 'test/src'

This directory named "src" doesn't exist. Am I missing something?

This directory named "src" doesn't exist. Am I missing something?

Hmm let me try with a clean sync of the patch. Back in a moment...

This directory named "src" doesn't exist. Am I missing something?

Hmm let me try with a clean sync of the patch. Back in a moment...

Oh you need to be one directory down - in test_event\test.

(The test directory in the test_event package has a src dir for the package testing tests, and a resources dir for data/files used by the test.)

Hey Zachary, forget about the test discovery part.

If you can just run this directly and let me know if that works, that'll be fine:

C:\Python35\python.exe D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event\test\src\TestCatchInvalidDecorator.py

That will be good enough to know the test can run on Windows. I may need to do a bit more homework to figure out how to get the discovery to work, but that's a completely orthogonal issue.

It can't find six, I guess because it's trying to run a script directly
rather than loading it as part of a package, so it never runs `import
use_lldb_suite`.

It can't find six, I guess because it's trying to run a script directly
rather than loading it as part of a package, so it never runs `import
use_lldb_suite`.

Ah of course. And this runs on OS X because six is part of the default Python site installation.

So an 'import use_lldb_suite' at the top of the event_collector file fixes it for you?

Well, I have to copy use_lldb_suite.py into lldbsuite/test_event first.
But then I get another error.

D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event>c:\Python35\python.exe
-m unittest discover -s test/src -p "Test*.py"

FF

FAIL: test_with_function_filter

(TestCatchInvalidDecorator.TestCatchInvalidDecorator)

Traceback (most recent call last):

File

"D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event\test\src\TestCatchInvalidDecorator.py",
line 56, in test_with_function_filter

"At least one job or test error result should have been returned")

AssertionError: 0 not greater than 0 : At least one job or test error
result should have been returned

FAIL: test_with_whole_file

(TestCatchInvalidDecorator.TestCatchInvalidDecorator)

Traceback (most recent call last):

File

"D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event\test\src\TestCatchInvalidDecorator.py",
line 37, in test_with_whole_file

"At least one job or test error result should have been returned")

AssertionError: 0 not greater than 0 : At least one job or test error
result should have been returned

I'm looking into a little bit. Currently I'm seeing that the temporary
events file is completely empty. I can run the same command line that
we're using in the subprocess.call() line, where in the code should I be
looking to see events getting written to the file?

Traceback (most recent call last):

File "D:\src\llvm\tools\lldb\test\dotest.py", line 7, in <module>
  lldbsuite.test.run_suite()
File "D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test\dotest.py",

line 897, in run_suite

  setupTestResults()
File "D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test\dotest.py",

line 481, in setupTestResults

  formatter_spec.formatter.handle_event(initialize_event)
File

"D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event\formatter\pickled.py",
line 72, in handle_event

  self.serializer.serialize(test_event, self.out_file)
File

"D:\src\llvm\tools\lldb\packages\Python\lldbsuite\test_event\formatter\pickled.py",
line 45, in serialize

cPickle.dump(test_event, out_file)

TypeError: write() argument must be str, not bytes
Press any key to continue . . .

This chunk of code:

with tempfile.NamedTemporaryFile(
        prefix="lldb_test_event_pickled_event_output",
        delete=False) as temp_file:
    return temp_file.name

inside event_collector.py comes up with a temp file name. (It opens and then immediately closes the file). That temp file name is then used as the file for which a "dotest.py" inferior will write output to, similar to how the real test suite has the dotest.py inferiors send messages over sockets. So the part that is really doing the writing is the dotest.py inferior.

Most likely, the dotest.py inferior isn't running. That means this subprocess is probably failing to run inside event_collector.py:

def _collect_events_with_command(command, events_filename):
    # Run the single test with dotest.py, outputting
    # the raw pickled events to a temp file.
    with open(os.devnull, 'w') as dev_null_file:
        subprocess.call(
            command,
            stdout=dev_null_file,
            stderr=dev_null_file)

    # Unpickle the events
    events = []
    if os.path.exists(events_filename):
        with open(events_filename, "rb") as events_file:
            while True:
                try:
                    # print("reading event")
                    event = cPickle.load(events_file)
                    # print("read event: {}".format(event))
                    if event:
                        events.append(event)
                except EOFError:
                    # This is okay.
                    break
        os.remove(events_filename)
    return events

There's a part that pipes that output to devnull. If you were to remove that piping to dev null, and just do this:

subprocess.call(command)

We'd probably see the actual error in trying to invoke dotest.py.

But at this point, I'd say you've helped a ton and put in more than enough time by verifying this isn't working quite yet. I will get a setup to repro and get it running there and address it directly.

Thanks, Zachary!

In lldbsuite\test_event\formatter\__init__.py, if I open the file with "wb"
instead of "w" I get further, but I still get other errors. This might
create a problem because you conditionally set the output file to stderr or
stdout, which are opened for text already, so those will never work with
cPickle.dump(). Do you have any good idea how to fix that?

In any case, for now I've set it to "wb" and I'm getting further. It's
returning results back to the unit test, but the results only contain a
begin and an end, but no error results.

Here's the patch I'm using so far to get to the point where dotest is
actually running and successfully returning results. It just doesn't
indicate any error results, only a job_begin and a job_end.

All this patch does is add use_lldb_suite.py in 2 places, import it, and
change a "w" to a "wb".

The question of why no errors are getting returned is still open.

Ok the problem is that it cannot find lldb.exe anywhere. Normally we use a
--executable argument when launching dotest.py, but there's no such
argument here., but

Ok the problem is that it cannot find lldb.exe anywhere. Normally we use a
--executable argument when launching dotest.py, but there's no such
argument here., but

Great, thanks for the patch!

I'll have a look at how we figure out where LLDB is for the real runner. I'm getting a repro setup together now.

Thanks again,
Todd

Zachary and I have been talking. I'm going to get this in, but I'll revisit the logic of the "test the test runner" test, which needs some work to get running on Windows.

This revision was automatically updated to reflect the committed changes.

Ahh wait. Does the "wb" issue still need to be resolved first, or is that
specific to testing the test?

Ahh wait. Does the "wb" issue still need to be resolved first, or is that
specific to testing the test?

That was specific to testing the test. It normally goes over a socket, which doesn't have that open issue.