-
Notifications
You must be signed in to change notification settings - Fork 66
Make test discovery from file fully lazy #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,9 @@ def test_discover_test_with_broken_import(self): | |
non-existent module is discovered.""" | ||
|
||
stdout, stderr = cmd_output( | ||
'python', '-m', 'testify.test_program', 'discovery_error', cwd='examples', | ||
'python', '-W', 'ignore::RuntimeWarning:runpy:', | ||
'-m', 'testify.test_program', 'discovery_error', | ||
cwd='examples', | ||
) | ||
|
||
T.assert_equal( | ||
|
@@ -37,7 +39,7 @@ def test_discover_test_with_broken_import(self): | |
r' submod = __import__\(module_name, fromlist=\[str\(\'__trash\'\)\]\)\n' | ||
r' File "[^"]+", line \d+, in <module>\n' | ||
r' import non_existent_module\n' | ||
r' ImportError: No module named \'?non_existent_module\'?\n' | ||
r' (ImportError|ModuleNotFoundError): No module named \'?non_existent_module\'?\n' | ||
), | ||
) | ||
|
||
|
@@ -49,7 +51,7 @@ def test_discover_test_with_broken_import(self): | |
r" submod = __import__\(module_name, fromlist=\[str\(\'__trash\'\)\]\)\n" | ||
r' File .+, line \d+, in <module>\n' | ||
r' import non_existent_module\n' | ||
r"ImportError: No module named '?non_existent_module'?\n" | ||
r"(ImportError|ModuleNotFoundError): No module named '?non_existent_module'?\n" | ||
), | ||
) | ||
|
||
|
@@ -65,6 +67,7 @@ def test_discover_test_with_unknown_import_error(self): | |
T.assert_in('AttributeError: aaaaa!', stderr) | ||
T.assert_in('AttributeError: aaaaa!', stdout) | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we split the lint / test fixes into a separate PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can do this once we agree that this change should actually be made to Testify |
||
if __name__ == '__main__': | ||
T.run() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
|
||
from collections import defaultdict | ||
import functools | ||
import itertools | ||
import inspect | ||
import types | ||
import unittest | ||
|
@@ -133,7 +134,7 @@ def __init__(self, *args, **kwargs): | |
|
||
self.__suites_exclude = kwargs.get('suites_exclude', set()) | ||
self.__suites_require = kwargs.get('suites_require', set()) | ||
self.__name_overrides = kwargs.get('name_overrides', None) | ||
self.__name_overrides = kwargs.get('name_overrides', itertools.repeat(None)) or itertools.repeat(None) | ||
|
||
TestResult.debug = kwargs.get('debugger') # sorry :( | ||
|
||
|
@@ -168,25 +169,37 @@ def runnable_test_methods(self): | |
any of our exclude_suites. If there are any require_suites, it will then further | ||
limit itself to test methods in those suites. | ||
""" | ||
for member_name in dir(self): | ||
if not member_name.startswith("test"): | ||
continue | ||
member = getattr(self, member_name) | ||
if not inspect.ismethod(member): | ||
continue | ||
|
||
member_suites = self.suites(member) | ||
|
||
# if there are any exclude suites, exclude methods under them | ||
if self.__suites_exclude and self.__suites_exclude & member_suites: | ||
continue | ||
# if there are any require suites, only run methods in *all* of those suites | ||
if self.__suites_require and not ((self.__suites_require & member_suites) == self.__suites_require): | ||
continue | ||
|
||
# if there are any name overrides, only run the named methods | ||
if self.__name_overrides is None or member.__name__ in self.__name_overrides: | ||
yield member | ||
already_inspected = set() | ||
name_overrides, self.__name_overrides = itertools.tee(self.__name_overrides) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm -1 on this, I don't believe it makes it any more lazy than the left hand side and at a great cost to readability |
||
|
||
for name_override in name_overrides: | ||
for member_name in dir(self): | ||
# No overrides were passed in, stop if we've seen all members already | ||
if not name_override: | ||
if member_name in already_inspected: | ||
return | ||
already_inspected.add(member_name) | ||
elif name_override == "__testify_abort_enumeration": | ||
return | ||
|
||
if not member_name.startswith("test"): | ||
continue | ||
member = getattr(self, member_name) | ||
if not inspect.ismethod(member): | ||
continue | ||
|
||
member_suites = self.suites(member) | ||
|
||
# if there are any exclude suites, exclude methods under them | ||
if self.__suites_exclude and self.__suites_exclude & member_suites: | ||
continue | ||
# if there are any require suites, only run methods in *all* of those suites | ||
if self.__suites_require and not ((self.__suites_require & member_suites) == self.__suites_require): | ||
continue | ||
|
||
# if there are any name overrides, only run the named methods | ||
if name_override is None or member.__name__ == name_override: | ||
yield member | ||
|
||
def run(self): | ||
"""Delegator method encapsulating the flow for executing a TestCase instance. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
from __future__ import absolute_import | ||
from itertools import chain | ||
from itertools import groupby | ||
from json import loads | ||
import sys | ||
|
@@ -8,6 +9,15 @@ | |
from .test_runner import TestRunner | ||
|
||
|
||
def readlines(fileobj): | ||
while True: | ||
line = fileobj.readline().rstrip() | ||
if line: | ||
yield line | ||
else: | ||
return | ||
|
||
|
||
class Test(namedtuple('TestTuple', ('module', 'cls', 'method'))): | ||
@classmethod | ||
def from_json(cls, json): | ||
|
@@ -32,23 +42,29 @@ def discover(self): | |
else: | ||
tests = open(self.filename) | ||
|
||
with tests as tests: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now this file doesn't get closed in a timely manner -- is this the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not, but I can close this file. |
||
tests = tests.read() | ||
test = tests.readline().rstrip() | ||
|
||
if not test: | ||
return | ||
|
||
if tests.startswith('{'): | ||
if test.startswith('{'): | ||
constructor = Test.from_json | ||
else: | ||
constructor = Test.from_name | ||
|
||
tests = [ | ||
constructor(test) | ||
for test in tests.splitlines() | ||
if test # Skip blank lines | ||
] | ||
# Put back the test name we peeked at to choose a constructor | ||
tests = chain( | ||
[constructor(test)], | ||
( | ||
constructor(test.rstrip()) | ||
for test in readlines(tests) | ||
if test # Skip blank lines | ||
), | ||
) | ||
|
||
for class_path, tests in groupby(tests, lambda test: test[:2]): | ||
methods = [test.method for test in tests] | ||
module, cls = class_path | ||
methods = (test.method for test in tests) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given it's already gone through |
||
|
||
cls = test_discovery.import_test_class(module, cls) | ||
yield self._construct_test(cls, name_overrides=methods) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curious: what's the
RuntimeWarning
that's happening here -- can we fix the code instead of silencing the warning?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's
RuntimeWarning: 'testify.test_program' found in sys.modules after import of package 'testify', but prior to execution of 'testify.test_program'; this may result in unpredic table behaviour
and I wasn't quite sure how to restructure testify itself to get around this issue