Skip to content

Commit

Permalink
Implement --picked=first, which will run the changed tests first, the…
Browse files Browse the repository at this point in the history
…n everything else afterwards (#23)

* Tweak --picked to allow choices of only, first

* Implement --picked=first to make changed things go first

* Apply black formatting

* plugin: _get_affected_paths now ValueErrors

* plugin: tuple unpacking return value for readability

* plugin: only reorder tests if there were matched paths

* Update CHANGELOG and README for --picked=first
  • Loading branch information
dsalisbury authored and anapaulagomes committed Dec 7, 2018
1 parent 9581d3d commit d3873da
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## [Unreleased]
### Added
- Added --picked=first mode, which will run all tests, but with any changed tests queued first

## [0.3.2] - 2018-11-25
### Added
Expand Down
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Usage

$ pytest --picked

$ pytest --picked=first

$ pytest --picked --mode=branch

$ pytest --picked --mode=unstaged # default
Expand All @@ -89,7 +91,10 @@ Usage
Features
--------

* Run tests from modified test files, according to ``git status``
Using ``git status``, this plugin allows you to:

* Run only tests from modified test files
* Run tests from modified test files first, followed by all unmodified tests

Installation
------------
Expand Down
6 changes: 1 addition & 5 deletions pytest_picked/modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


class Mode(ABC):

def __init__(self, test_file_convention):
self.test_file_convention = test_file_convention

Expand All @@ -28,8 +27,7 @@ def affected_tests(self):
return files, folders

def git_output(self):
output = subprocess.run( # nosec
self.command(), stdout=subprocess.PIPE)
output = subprocess.run(self.command(), stdout=subprocess.PIPE) # nosec
return output.stdout.decode("utf-8")

def print_command(self):
Expand All @@ -51,7 +49,6 @@ def parser(self, candidate):


class Branch(Mode):

def command(self):
return ["git", "diff", "--name-only", "master"]

Expand All @@ -61,7 +58,6 @@ def parser(self, candidate):


class Unstaged(Mode):

def command(self):
return ["git", "status", "--short"]

Expand Down
50 changes: 40 additions & 10 deletions pytest_picked/plugin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from fnmatch import fnmatch
import _pytest

from .modes import Branch, Unstaged
Expand All @@ -7,9 +8,15 @@ def pytest_addoption(parser):
group = parser.getgroup("picked")
group.addoption(
"--picked",
action="store_true",
action="store",
dest="picked",
help="Run the tests related to the changed files",
choices=("only", "first"),
nargs="?",
const="only",
help=(
"Run the tests related to the changed files either on their own, "
"or first"
),
)
group.addoption(
"--mode",
Expand All @@ -21,11 +28,7 @@ def pytest_addoption(parser):
)


def pytest_configure(config):
picked_plugin = config.getoption("picked")
if not picked_plugin:
return

def _get_affected_paths(config):
picked_mode = config.getoption("picked_mode")
test_file_convention = config._getini( # pylint: disable=W0212
"python_files"
Expand All @@ -41,12 +44,39 @@ def pytest_configure(config):
error = "Invalid mode. Options: `{}`.".format(", ".join(modes.keys()))
_write(config, [error])
config.args = []
raise ValueError(error)
else:
picked_files, picked_folders = mode.affected_tests()
return mode.affected_tests()


config.args = picked_files + picked_folders
def pytest_configure(config):
picked_type = config.getoption("picked")
if not picked_type or picked_type != "only":
return

picked_files, picked_folders = _get_affected_paths(config)
config.args = picked_files + picked_folders
_display_affected_tests(config, picked_files, picked_folders)


def pytest_collection_modifyitems(session, config, items):
picked_type = config.getoption("picked")
if not picked_type or picked_type != "first":
return

_display_affected_tests(config, picked_files, picked_folders)
affected_files, affected_folders = _get_affected_paths(config)
match_paths = affected_files + affected_folders
# only reorder if there was anything matched
if match_paths:
run_first = []
run_later = []
for item in items:
item_path = item.location[0]
if any(fnmatch(item_path, m) for m in match_paths):
run_first.append(item)
else:
run_later.append(item)
items[:] = run_first + run_later


def _display_affected_tests(config, files, folders):
Expand Down
2 changes: 0 additions & 2 deletions tests/test_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@


class TestUnstaged:

def test_should_return_git_status_command(self):
mode = Unstaged([])
command = mode.command()
Expand Down Expand Up @@ -74,7 +73,6 @@ def test_should_list_unstaged_changed_files_as_affected_tests(self):


class TestBranch:

def test_should_return_command_that_list_all_changed_files(self):
mode = Branch([])
command = mode.command()
Expand Down
80 changes: 78 additions & 2 deletions tests/test_pytest_picked.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from unittest.mock import patch
import pytest


def test_shows_affected_tests(testdir):
Expand All @@ -11,11 +12,28 @@ def test_shows_affected_tests(testdir):
def test_help_message(testdir):
result = testdir.runpytest("--help")

result.stdout.fnmatch_lines(
["picked:", "*--picked*Run the tests related to the changed files"]
result.stdout.re_match_lines(
[
"^picked:$",
r"^\s+--picked=\[{only,first}\]$",
r"^\s+Run the tests related to the changed files either on",
r"^\s+their own, or first",
]
)


@pytest.mark.parametrize("picked_type", [None, "only"])
def test_picked_type_options(testdir, picked_type):
with patch("pytest_picked.modes.subprocess.run") as subprocess_mock:
subprocess_mock.return_value.stdout = b""

result = testdir.runpytest(
"--picked={}".format(picked_type) if picked_type else "--picked"
)

result.stdout.fnmatch_lines(["Changed test files... 0. []"])


def test_filter_items_according_with_git_status(testdir, tmpdir):
with patch("pytest_picked.modes.subprocess.run") as subprocess_mock:
output = b" M test_flows.py\n M test_serializers.py\n A tests/\n"
Expand Down Expand Up @@ -176,3 +194,61 @@ def test_should_not_run_the_tests_if_mode_is_invalid(testdir, tmpdir):

result = testdir.runpytest("--picked", "--mode=random")
result.stdout.re_match_lines(["Invalid mode. Options: "])


def test_picked_first_orders_tests_correctly(testdir, tmpdir):
with patch("pytest_picked.modes.subprocess.run") as subprocess_mock:
output = b" M test_flows.py\n M test_serializers.py\n"
subprocess_mock.return_value.stdout = output

testdir.makepyfile(
test_access="""
def test_sth():
assert True
""",
test_flows="""
def test_sth():
assert True
""",
test_serializers="""
def test_sth():
assert True
""",
test_views="""
def test_sth():
assert True
""",
)
result = testdir.runpytest("--picked=first", "-v")
result.stdout.re_match_lines(
[
"test_flows.py.+",
"test_serializers.py.+",
"test_access.py.+",
"test_views.py.+",
]
)


def test_picked_first_but_nothing_changed(testdir, tmpdir):
with patch("pytest_picked.modes.subprocess.run") as subprocess_mock:
output = b"\n"
subprocess_mock.return_value.stdout = output

testdir.makepyfile(
test_access="""
def test_sth():
assert True
""",
test_flows="""
def test_sth():
assert True
""",
)
result = testdir.runpytest("--picked=first", "-v")
result.stdout.re_match_lines(
[
"test_access.py.+",
"test_flows.py.+",
]
)

0 comments on commit d3873da

Please sign in to comment.