-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
#3034 Added new option "--new-first" #3218
#3034 Added new option "--new-first" #3218
Conversation
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.
Awesome work @feuillemorte, this is a great first pass. Please take a look at my comments and see what you think. 👍
_pytest/cacheprovider.py
Outdated
def __init__(self, config): | ||
self.config = config | ||
self.active = config.option.newfirst | ||
self.all_items = config.cache.get("cache/allitems", {}) |
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.
I believe the default value here should be []
, for consistency more than anything else.
_pytest/cacheprovider.py
Outdated
other_items = [] | ||
for item in items: | ||
mod_timestamp = os.path.getmtime(str(item.fspath)) | ||
if self.all_items and item.nodeid not in self.all_items: |
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.
Unfortunately item.nodeid not in self.all_items
will be a linear search, which can be expensive if we have a large collection.
I think you should change new_items
and other_items
to a dict(nodeid -> timestamp)
instead, given that you will be sorting them later anyway.
Also you can check just with if item.nodeid not in self.all_items:
because and empty list or dict will return False
for the containment check.
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.
the code is incorrect about the typo of all_items, it changes multiple times
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.
@RonnyPfannschmidt You mean I need to rename it to smth like "cached_items"?
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.
I think you should change new_items and other_items to a dict(nodeid -> timestamp) instead, given that you will be sorting them later anyway.
In this case I will not know about new tests, isn't it? Only about modified files
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.
In this case I will not know about new tests, isn't it? Only about modified files
I did not explain correctly sorry, I meant this:
new_items = {}
other_items = {}
for item in items:
if item.nodeid not in self.all_items:
new_items[item.nodeid] = item
else:
other_items[item.nodeid] = item
And you can get the timestamp during your sorting function:
items[:] = self._get_increasing_order(six.itervalues(new_items)) + \
self._get_increasing_order(six.itervalues(other_items.values))
...
def _get_increasing_order(self, items):
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
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.
@nicoddemus this is not working in py27. Every time different order:
test_2/test_2.py::test_1[1] PASSED
test_2/test_2.py::test_1[2] PASSED
test_1/test_1.py::test_1[2] PASSED
test_1/test_1.py::test_1[1] PASSED
I think it's better to save my code based on lists. What do you think?
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.
Try changing new_items
and other_items
to a OrderedDict
instead, that should be equivalent to the previous code.
_pytest/cacheprovider.py
Outdated
if config.getoption("cacheshow") or hasattr(config, "slaveinput"): | ||
return | ||
config.cache.set("cache/allitems", | ||
[item.nodeid for item in self.all_items if isinstance(item, Function)]) |
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.
Why are you testing isinstance
here? 🤔
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.
Please, check testing/acceptance_test.py:584
pytest/testing/acceptance_test.py
Lines 580 to 585 in 063e2da
monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir)) | |
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) | |
assert result.ret != 0 | |
result.stderr.fnmatch_lines([ | |
"*not*found*test_missing*", | |
]) |
It failes because sometimes item is a string, not object:
AttributeError: 'unicode' object has no attribute 'nodeid'
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.
I see thanks. Sometimes pytest_collect_modifyitems
can received strings, weird. Didn't take the time to investigate this further, but regardless I think it is safer to check for pytest.Item
instead while still in pytest_collect_modifyitems
:
self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)]
(Also already suggesting renaming all_items
to cached_nodeids
). During pytest_sessionfinish
you can dump it directly:
config.cache.set("cache/allitems", self.cached_nodeids )
_pytest/cacheprovider.py
Outdated
@@ -179,6 +216,10 @@ def pytest_addoption(parser): | |||
help="run all tests but run the last failures first. " | |||
"This may re-order tests and thus lead to " | |||
"repeated fixture setup/teardown") | |||
group.addoption( | |||
'--nf', '--new-first', action='store_true', dest="newfirst", | |||
help="run all tests but run new tests first, then tests from " |
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.
I think this reads better: run tests from new files first, then the rest of the tests sorted by file mtime
doc/en/cache.rst
Outdated
New tests first | ||
----------------- | ||
|
||
The plugin provides command line options to run tests in another order: |
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.
I think this should be moved closer to --lf
, right before the The new config.cache object
section.
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.
I also think we should use the same blurb from the changelog. 👍
changelog/3034.feature
Outdated
@@ -0,0 +1 @@ | |||
Added new option `--nf`, `--new-first`. This option enables run tests in next order: first new tests, then last modified files with tests in descending order (default order inside file). |
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.
Suggestion:
New ``--nf``, ``--new-first`` options: run new tests first followed by the rest of the tests, in both cases tests are also sorted by the file modified time, with more recent files coming first.
testing/test_cacheprovider.py
Outdated
|
||
class TestNewFirst(object): | ||
def test_newfirst_usecase(self, testdir): | ||
t1 = testdir.mkdir("test_1") |
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.
You can write those files more easily with makepyfile
:
testdir.makepyfile(**{
'test_1/test_1.py': '''
def test_1(): assert 1
def test_2(): assert 1
def test_3(): assert 1
''',
'test_1/test_2.py': '''
def test_1(): assert 1
def test_2(): assert 1
def test_3(): assert 1
'''
})
testing/test_cacheprovider.py
Outdated
) | ||
|
||
path_to_test_1 = str('{}/test_1/test_1.py'.format(testdir.tmpdir)) | ||
os.utime(path_to_test_1, (1, 1)) |
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.
You can use setmtime here for the same effect.
testdir.tmpdir.join('test_1/test_1.py').setmtime(1)
testing/test_cacheprovider.py
Outdated
]) | ||
|
||
def test_newfirst_parametrize(self, testdir): | ||
t1 = testdir.mkdir("test_1") |
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.
Same suggestions here about using testdir
. 👍
_pytest/cacheprovider.py
Outdated
new_items = [] | ||
other_items = [] | ||
for item in items: | ||
mod_timestamp = os.path.getmtime(str(item.fspath)) |
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.
@nicoddemus
Will it work on windows? Could you test if you work on windows, please?
_pytest/cacheprovider.py
Outdated
new_items = [] | ||
other_items = [] | ||
for item in items: | ||
mod_timestamp = os.path.getmtime(str(item.fspath)) |
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.
use item.fspath.mtime()
here - it works on windows btw, the tests passedthere at least
_pytest/cacheprovider.py
Outdated
other_items = [] | ||
for item in items: | ||
mod_timestamp = os.path.getmtime(str(item.fspath)) | ||
if self.all_items and item.nodeid not in self.all_items: |
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.
the code is incorrect about the typo of all_items, it changes multiple times
_pytest/cacheprovider.py
Outdated
other_items = [] | ||
for item in items: | ||
mod_timestamp = os.path.getmtime(str(item.fspath)) | ||
if self.all_items and item.nodeid not in self.all_items: |
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.
In this case I will not know about new tests, isn't it? Only about modified files
I did not explain correctly sorry, I meant this:
new_items = {}
other_items = {}
for item in items:
if item.nodeid not in self.all_items:
new_items[item.nodeid] = item
else:
other_items[item.nodeid] = item
And you can get the timestamp during your sorting function:
items[:] = self._get_increasing_order(six.itervalues(new_items)) + \
self._get_increasing_order(six.itervalues(other_items.values))
...
def _get_increasing_order(self, items):
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
_pytest/cacheprovider.py
Outdated
if config.getoption("cacheshow") or hasattr(config, "slaveinput"): | ||
return | ||
config.cache.set("cache/allitems", | ||
[item.nodeid for item in self.all_items if isinstance(item, Function)]) |
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.
I see thanks. Sometimes pytest_collect_modifyitems
can received strings, weird. Didn't take the time to investigate this further, but regardless I think it is safer to check for pytest.Item
instead while still in pytest_collect_modifyitems
:
self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)]
(Also already suggesting renaming all_items
to cached_nodeids
). During pytest_sessionfinish
you can dump it directly:
config.cache.set("cache/allitems", self.cached_nodeids )
…o 3034-new-tests-first
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.
Thanks @feuillemorte!
@RonnyPfannschmidt please, review :) |
@@ -56,7 +56,7 @@ def test_error(): | |||
assert result.ret == 1 | |||
result.stdout.fnmatch_lines([ | |||
"*could not create cache path*", | |||
"*1 warnings*", | |||
"*2 warnings*", |
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.
What is the new warning here? Shouldn't it get matched also?
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.
1st warning is
could not create cache path .pytest_cache/v/cache/lastfailed
and second is for new-first. don't need to create another test for it
Fixes #3034
Added new option --new-first
How it works:
Create different test files:
tests
|- test_one.py
- test_two.py
Every file contains two tests:
run command
result:
run command
result:
Order changed to modifyed files first
Add new test to test_two.py file and then modify test_one.py file
run command
result:
Order changed: first new test, then modified files