Skip to content

Commit fef6d4a

Browse files
authored
Add pytest_add_cli_args and pytest_add_cli_args_test_selection configs (#440)
* Add pytest_add_cli_args and pytest_add_cli_args_test_selection configs * Only list selected tests Necessary if we run `mutmut run` twice with some pytest_add_cli_args_test_selection. * Document pytest_add_cli_args_test_selection instead of tests_dir
1 parent 2acbf1f commit fef6d4a

File tree

5 files changed

+106
-49
lines changed

5 files changed

+106
-49
lines changed

README.rst

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ source code control and committed before you apply a mutant!
6767
If during the installation you get an error for the `libcst` dependency mentioning the lack of a rust compiler on your system, it is because your architecture does not have a prebuilt binary for `libcst` and it requires both `rustc` and `cargo` from the [rust toolchain](https://www.rust-lang.org/tools/install) to be built. This is known for at least the `x86_64-darwin` architecture.
6868

6969

70+
Wildcards for testing mutants
71+
-----------------------------
72+
73+
Unix filename pattern matching style on mutants is supported. Example:
74+
75+
.. code-block:: console
76+
77+
mutmut run "my_module*"
78+
mutmut run "my_module.my_function*"
79+
80+
In the `browse` TUI you can press `f` to retest a function, and `m` to retest
81+
an entire module.
82+
83+
7084
Configuration
7185
-------------
7286

@@ -76,35 +90,21 @@ In `setup.cfg` in the root of your project you can configure mutmut if you need
7690
7791
[mutmut]
7892
paths_to_mutate=src/
79-
tests_dir=tests/
93+
pytest_add_cli_args_test_selection=tests/
8094
8195
If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutmut` section:
8296

8397
.. code-block:: toml
8498
8599
[tool.mutmut]
86100
paths_to_mutate = [ "src/" ]
87-
tests_dir = [ "tests/" ]
101+
pytest_add_cli_args_test_selection= [ "tests/" ]
88102
89103
See below for more options for configuring mutmut.
90104

91105

92-
Wildcards for testing mutants
93-
-----------------------------
94-
95-
Unix filename pattern matching style on mutants is supported. Example:
96-
97-
.. code-block:: console
98-
99-
mutmut run "my_module*"
100-
mutmut run "my_module.my_function*"
101-
102-
In the `browse` TUI you can press `f` to retest a function, and `m` to retest
103-
an entire module.
104-
105-
106106
"also copy" files
107-
-----------------
107+
~~~~~~~~~~~~~~~~~
108108

109109
To run the full test suite some files are often needed above the tests and the
110110
source. You can configure to copy extra files that you need by adding
@@ -118,7 +118,7 @@ directories and files to `also_copy` in your `setup.cfg`:
118118
119119
120120
Limit stack depth
121-
-----------------
121+
~~~~~~~~~~~~~~~~~
122122

123123
In big code bases some functions are called incidentally by huge swaths of the
124124
codebase, but you really don't want tests that hit those executions to count
@@ -142,7 +142,7 @@ caught.
142142

143143

144144
Exclude files from mutation
145-
---------------------------
145+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
146146

147147
You can exclude files from mutation in `setup.cfg`:
148148

@@ -153,7 +153,7 @@ You can exclude files from mutation in `setup.cfg`:
153153
154154
155155
Enable coverage.py filtering of lines to mutate
156-
-----------------------------------------------
156+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
157157

158158
By default, mutmut will mutate only functions that are called. But, if you would like a finer grained (line-level)
159159
check for coverage, mutmut can use coverage.py to do that.
@@ -168,7 +168,7 @@ If you only want to mutate lines that are called (according to coverage.py), you
168168
169169
170170
Enable debug output (increase verbosity)
171-
----------------------------------------
171+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172172

173173
By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
174174

@@ -182,7 +182,7 @@ to failing tests.
182182
183183
184184
Whitelisting
185-
------------
185+
~~~~~~~~~~~~
186186

187187
You can mark lines like this:
188188

@@ -198,6 +198,27 @@ whitelist lines are:
198198
to continue, but it's slower.
199199

200200

201+
Modifying pytest arguments
202+
~~~~~~~~~~~~~~~~~~~~~~~~~~
203+
204+
You can add and override pytest arguments:
205+
206+
.. code-block:: python
207+
208+
# for CLI args that select or deselect tests, use `pytest_add_cli_args_test_selection`
209+
pytest_add_cli_args_test_selection = ["-m", "not fail", "-k=test_include"]
210+
211+
# for other CLI args, use `pytest_add_cli_args`
212+
pytest_add_cli_args = ["-p", "no:some_plugin"] # disable a plugin
213+
pytest_add_cli_args = ["-o", "xfail_strict=False"] # overrides xfail_strict from your normal config
214+
215+
# if you want to ignore the normal pytest configuration
216+
# you can specify a diferent pytest ini file to be used
217+
pytest_add_cli_args = ["-c", "mutmut_pytest.ini"]
218+
also_copy = ["mutmut_pytest.ini"]
219+
220+
221+
201222
Example mutations
202223
-----------------
203224

e2e_projects/config/pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@ do_not_mutate = [ "*ignore*" ]
2929
also_copy = [ "data" ]
3030
max_stack_depth=8 # Includes frames by mutmut, see https://github.com/boxed/mutmut/issues/378
3131
tests_dir = [ "tests/main/" ]
32+
# verify that we can override options with pytest_add_cli_args
33+
pytest_add_cli_args = ["-o", "xfail_strict=False"]
34+
# verify test exclusion (-m 'not fail') and test inclusion (-k=test_include)
35+
pytest_add_cli_args_test_selection = [ "-m", "not fail", "-k=test_include"]
36+
37+
[tool.pytest.ini_options]
38+
xfail_strict = true
39+
markers = [ "fail: tests that should be ignored with mutmut" ]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from config_pkg.math import func_with_no_tests
22

3-
def test_func_with_no_tests():
3+
# ignored, because tests_dir specifies only the main directory
4+
def test_include_func_with_no_tests():
45
assert func_with_no_tests() == 420
Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
import json
2+
import pytest
23
from pathlib import Path
34
from config_pkg import hello
45
from config_pkg.math import add, call_depth_two
56
from config_pkg.ignore_me import this_function_shall_NOT_be_mutated
67

7-
def test_hello():
8+
def test_include_hello():
89
assert hello() == "Hello from config!"
910

10-
def test_add():
11+
def test_include_add():
1112
assert add(1, 0) == 1
1213

13-
def test_non_mutated_function():
14+
def test_include_non_mutated_function():
1415
assert this_function_shall_NOT_be_mutated() == 3
1516

16-
def test_max_stack_depth():
17+
def test_include_max_stack_depth():
1718
# This test should only cover functions up to some depth
1819
# For more context, see https://github.com/boxed/mutmut/issues/378
1920
assert call_depth_two() == 2
2021

21-
def test_data_exists():
22+
def test_include_data_exists():
2223
path = (Path("data") / "data.json").resolve()
2324
assert path.exists()
2425
with open(path) as f:
2526
data = json.load(f)
26-
assert data['comment'] == 'this should be copied to the mutants folder'
27+
assert data['comment'] == 'this should be copied to the mutants folder'
28+
29+
# ignored, because it does not match -k 'test_include'
30+
def test_should_be_ignored():
31+
assert 'This test should be ignored' == 1234
32+
33+
@pytest.mark.xfail
34+
def test_include_xfail_that_does_not_fail():
35+
# verify that we can override the xfail=strict from the pytest settings
36+
assert 1 == 1
37+
38+
# ignored, because of -m 'not fail'
39+
@pytest.mark.fail
40+
def test_include_that_should_be_ignored():
41+
assert 'This test should be ignored' == 1234

mutmut/__main__.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -420,13 +420,22 @@ def new_tests(self):
420420

421421

422422
class PytestRunner(TestRunner):
423+
def __init__(self):
424+
self._pytest_add_cli_args: List[str] = mutmut.config.pytest_add_cli_args
425+
self._pytest_add_cli_args_test_selection: List[str] = mutmut.config.pytest_add_cli_args_test_selection
426+
427+
# tests_dir is a special case of a test selection option,
428+
# so also use pytest_add_cli_args_test_selection for the implementation
429+
self._pytest_add_cli_args_test_selection += mutmut.config.tests_dir
430+
431+
423432
# noinspection PyMethodMayBeStatic
424433
def execute_pytest(self, params: list[str], **kwargs):
425434
import pytest
426-
params += ['--rootdir=.']
435+
params = ['--rootdir=.'] + params + self._pytest_add_cli_args
427436
if mutmut.config.debug:
428437
params = ['-vv'] + params
429-
print('python -m pytest ', ' '.join(params))
438+
print('python -m pytest ', ' '.join([f'"{param}"' for param in params]))
430439
exit_code = int(pytest.main(params, **kwargs))
431440
if mutmut.config.debug:
432441
print(' exit code', exit_code)
@@ -457,9 +466,7 @@ def pytest_runtest_makereport(self, item, call):
457466
if tests:
458467
pytest_args += list(tests)
459468
else:
460-
tests_dir = mutmut.config.tests_dir
461-
if tests_dir:
462-
pytest_args += tests_dir
469+
pytest_args += self._pytest_add_cli_args_test_selection
463470
with change_cwd('mutants'):
464471
return int(self.execute_pytest(pytest_args, plugins=[stats_collector]))
465472

@@ -468,38 +475,39 @@ def run_tests(self, *, mutant_name, tests):
468475
if tests:
469476
pytest_args += list(tests)
470477
else:
471-
tests_dir = mutmut.config.tests_dir
472-
if tests_dir:
473-
pytest_args += tests_dir
478+
pytest_args += self._pytest_add_cli_args_test_selection
474479
with change_cwd('mutants'):
475480
return int(self.execute_pytest(pytest_args))
476481

477482
def run_forced_fail(self):
478-
pytest_args = ['-x', '-q']
479-
tests_dir = mutmut.config.tests_dir
480-
if tests_dir:
481-
pytest_args += tests_dir
483+
pytest_args = ['-x', '-q'] + self._pytest_add_cli_args_test_selection
482484
with change_cwd('mutants'):
483485
return int(self.execute_pytest(pytest_args))
484486

485487
def list_all_tests(self):
486488
class TestsCollector:
489+
def __init__(self):
490+
self.collected_nodeids = set()
491+
self.deselected_nodeids = set()
492+
487493
def pytest_collection_modifyitems(self, items):
488-
self.nodeids = {item.nodeid for item in items}
494+
self.collected_nodeids |= {item.nodeid for item in items}
495+
496+
def pytest_deselected(self, items):
497+
self.deselected_nodeids |= {item.nodeid for item in items}
489498

490499
collector = TestsCollector()
491500

492501
tests_dir = mutmut.config.tests_dir
493-
pytest_args = ['-x', '-q', '--collect-only']
494-
if tests_dir:
495-
pytest_args += tests_dir
502+
pytest_args = ['-x', '-q', '--collect-only'] + self._pytest_add_cli_args_test_selection
496503

497504
with change_cwd('mutants'):
498505
exit_code = int(self.execute_pytest(pytest_args, plugins=[collector]))
499506
if exit_code != 0:
500507
raise CollectTestsFailedException()
501508

502-
return ListAllTestsResult(ids=collector.nodeids)
509+
selected_nodeids = collector.collected_nodeids - collector.deselected_nodeids
510+
return ListAllTestsResult(ids=selected_nodeids)
503511

504512

505513
class HammettRunner(TestRunner):
@@ -701,8 +709,10 @@ class Config:
701709
max_stack_depth: int
702710
debug: bool
703711
paths_to_mutate: List[Path]
704-
tests_dir: List[str] = None
705-
mutate_only_covered_lines: bool = False
712+
pytest_add_cli_args: List[str]
713+
pytest_add_cli_args_test_selection: List[str]
714+
tests_dir: List[str]
715+
mutate_only_covered_lines: bool
706716

707717
def should_ignore_for_mutation(self, path):
708718
if not str(path).endswith('.py'):
@@ -739,7 +749,7 @@ def s(key, default):
739749
config_parser = ConfigParser()
740750
config_parser.read('setup.cfg')
741751

742-
def s(key, default):
752+
def s(key: str, default):
743753
try:
744754
result = config_parser.get('mutmut', key)
745755
except (NoOptionError, NoSectionError):
@@ -784,6 +794,8 @@ def load_config():
784794
for y in s('paths_to_mutate', [])
785795
] or guess_paths_to_mutate(),
786796
tests_dir=s('tests_dir', []),
797+
pytest_add_cli_args=s('pytest_add_cli_args', []),
798+
pytest_add_cli_args_test_selection=s('pytest_add_cli_args_test_selection', []),
787799
)
788800

789801

0 commit comments

Comments
 (0)