Skip to content

Refactor test discovery#3973

Merged
JukkaL merged 17 commits intopython:masterfrom
elazarg:refactor-test-discovery
Dec 12, 2017
Merged

Refactor test discovery#3973
JukkaL merged 17 commits intopython:masterfrom
elazarg:refactor-test-discovery

Conversation

@elazarg
Copy link
Copy Markdown
Contributor

@elazarg elazarg commented Sep 20, 2017

Test discovery logic is duplicated over all the different test suites. This PR refactors this logic to the plugin method collect(), leaving only the choice of configuration in the classes. This refactoring should enable further optimizations in the discovery of specific tests.

The argument perform to parse_test_cases() is (already) unused, so it is removed. Same goes for include_path.

The core of the changes can be tested by diffing the result of py.test --collect-only -n0 with the main branch. The only differences should be the execution time and the name of the suite PythonCmdlineSuite which is called PythonEvaluationSuite on master.

TestCase is split into BaseTestCase to avoid sharing of fields and methods that's not really shared with myunit tests, such as run(). I believe this change need not last long.

@elazarg elazarg force-pushed the refactor-test-discovery branch 7 times, most recently from d75d7ee to a107d0c Compare September 20, 2017 09:15
@elazarg elazarg force-pushed the refactor-test-discovery branch from e76f9ef to 5df8a6c Compare October 21, 2017 19:13
@elazarg elazarg force-pushed the refactor-test-discovery branch from 5df8a6c to 34120f6 Compare October 21, 2017 20:04
Copy link
Copy Markdown
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negative lines of code is great -- thanks! This removes some pretty pointless boilerplate. I have just a bunch of style/documentation related comments.

def __init__(self, name: str, suite: 'Optional[Suite]' = None,
func: Optional[Callable[[], None]] = None) -> None:
self.func = func
class ProtoTestCase:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest renaming this to BaseTestCase, since Proto sounds like a protocol but this is a regular class. Also add a docstring. At least describe how these are run, since this class doesn't seem to have a hint about that.

self.tmpdir = None


class TestCase(ProtoTestCase):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstring. In particular, how is this different from the base class?



class DataDrivenTestCase(TestCase):
class DataDrivenTestCase(ProtoTestCase):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of confusing, since there seems to be no longer anything in this class about how an individual test case is run. This at least needs a docstring.

def pytest_pycollect_makeitem(collector: Any, name: str, obj: Any) -> Any:
if not isinstance(obj, type) or not issubclass(obj, DataSuite):
return None
if obj is DataSuite:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment here -- something like this perhaps: only classes derived from DataSuite contain test cases, not the DataSuite class.

def collect(self) -> Iterator['MypyDataCase']:
for case in self.obj.cases():
yield MypyDataCase(case.name, self, case)
cls = self.obj
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment -- what is self.obj here and where does it come from? This apparently requires understanding some pytest magic, which many readers likely won't be too familiar with, including me.

return MypyDataSuite(name, parent=collector)


class MypyDataSuite(pytest.Class): # type: ignore # inheriting from Any
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add docstring here as well while you are at it and probably have more context on this than I have?



class DataSuite:
files = None # type: typing.ClassVar[List[str]]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not have the ClassVar annotations here and elsewhere -- they add noise and distract from the main intent of the code. It would be okay to just add a comment here mentioning that all of these should be treated as class variables.

optional_out = False # type: typing.ClassVar[bool]
native_sep = False # type: typing.ClassVar[bool]
require_stable = False # type: typing.ClassVar[bool]
require_incremental = False # type: typing.ClassVar[bool]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require_stable and require_incremental are too specialized to be included here as configuration attributes. It's okay to have is_incremental and has_stable_flags as utility functions in this module (I don't think that they need to be associated with any class), but I'd prefer if these would be explicitly handled using asserts in run_case methods, similar to how these were implemented previously.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are also used as filters in the test selection, and the asserts only validate it. I will add a filter method then.

test_python_evaluation, test_temp_dir, True)
return c
files = ['pythoneval.test', 'python2eval.test'] # type: typing.ClassVar[List[str]]
if sys.version_info.major == 3 and sys.version_info.minor >= 4:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.3 is no longer supported for running mypy so this test can be dropped.

@elazarg
Copy link
Copy Markdown
Contributor Author

elazarg commented Dec 1, 2017

I think I've addressed the comments except the parts about configuration options. I hope to address the rest tomorrow.

@elazarg
Copy link
Copy Markdown
Contributor Author

elazarg commented Dec 2, 2017

@JukkaL I believe this is ready.

Copy link
Copy Markdown
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! Looks mostly good now, just a few minor things left.

class BaseTestCase:
"""Common base class for _MyUnitTestCase and DataDrivenTestCase.

Handles temporary folder creation and deletion"""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: In multi-line docstrings add the trailing """ on a separate line like this:

class BaseTestCase:
    """Common base class for _MyUnitTestCase and DataDrivenTestCase.

    Handles temporary folder creation and deletion.
    """
    ...


def setup(self) -> None:
super().setup()
self.suite.set_up()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent naming: setup vs set_up?

obj: object) -> 'Optional[Any]':
"""Called by pytest on each object in modules configured in conftest.py files.

collector is pytest.Collector, returns Optional[pytest.Class]"""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: """ should be on a separate line in multi-line docstrings.

# Only classes derived from DataSuite contain test cases, not the DataSuite class itself
if issubclass(obj, DataSuite) and obj is not DataSuite:
# Non-None result means this obj is a test case
# result.collect will be called, with self.obj being obj
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it's a little unclear what result.collect refers to. Maybe rephrase as "The collect method of the returned MypyDataSuite instance will be called later, with self.obj being obj."

c += parse_test_cases(os.path.join(test_data_prefix, f),
None, test_temp_dir, True)
return c
files = typecheck_files # type: List[str]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the variable type annotations for in DataSuite subclasses now seem redundant. Can you remove them?

def run_case(self, testcase: DataDrivenTestCase) -> None:
assert self.is_incremental(testcase), "Testcase is not incremental"
assert self.has_stable_flags(testcase), "Testcase has varying flags"
assert has_stable_flags(testcase), "Testcase is not incremental"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message should be "Testcase has varying flags" (the messages seem to have been switched on these two lines).

assert self.is_incremental(testcase), "Testcase is not incremental"
assert self.has_stable_flags(testcase), "Testcase has varying flags"
assert has_stable_flags(testcase), "Testcase is not incremental"
assert is_incremental(testcase), "Testcase has varying flags"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the other switched message.

@elazarg
Copy link
Copy Markdown
Contributor Author

elazarg commented Dec 12, 2017

Thanks! Fixed.

@JukkaL JukkaL merged commit dc52e86 into python:master Dec 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants