diff --git a/CHANGELOG.md b/CHANGELOG.md index e91869c..c460522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.rst b/README.rst index b095fa7..a6b5964 100644 --- a/README.rst +++ b/README.rst @@ -81,6 +81,8 @@ Usage $ pytest --picked + $ pytest --picked=first + $ pytest --picked --mode=branch $ pytest --picked --mode=unstaged # default @@ -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 ------------ diff --git a/pytest_picked/modes.py b/pytest_picked/modes.py index d12eb32..fc8ede0 100644 --- a/pytest_picked/modes.py +++ b/pytest_picked/modes.py @@ -4,7 +4,6 @@ class Mode(ABC): - def __init__(self, test_file_convention): self.test_file_convention = test_file_convention @@ -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): @@ -51,7 +49,6 @@ def parser(self, candidate): class Branch(Mode): - def command(self): return ["git", "diff", "--name-only", "master"] @@ -61,7 +58,6 @@ def parser(self, candidate): class Unstaged(Mode): - def command(self): return ["git", "status", "--short"] diff --git a/pytest_picked/plugin.py b/pytest_picked/plugin.py index c77c59e..01463f1 100644 --- a/pytest_picked/plugin.py +++ b/pytest_picked/plugin.py @@ -1,3 +1,4 @@ +from fnmatch import fnmatch import _pytest from .modes import Branch, Unstaged @@ -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", @@ -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" @@ -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): diff --git a/tests/test_modes.py b/tests/test_modes.py index c6762b8..97fa8a3 100644 --- a/tests/test_modes.py +++ b/tests/test_modes.py @@ -6,7 +6,6 @@ class TestUnstaged: - def test_should_return_git_status_command(self): mode = Unstaged([]) command = mode.command() @@ -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() diff --git a/tests/test_pytest_picked.py b/tests/test_pytest_picked.py index 0f5c6b6..e472989 100644 --- a/tests/test_pytest_picked.py +++ b/tests/test_pytest_picked.py @@ -1,4 +1,5 @@ from unittest.mock import patch +import pytest def test_shows_affected_tests(testdir): @@ -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" @@ -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.+", + ] + )