mutmut 3.3.0__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mutmut-3.3.0 → mutmut-3.3.1}/HISTORY.rst +16 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/PKG-INFO +18 -1
- {mutmut-3.3.0 → mutmut-3.3.1}/README.rst +17 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/.DS_Store +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__init__.py +1 -1
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__main__.py +109 -36
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/file_mutation.py +3 -33
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/node_mutation.py +39 -12
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/trampoline_templates.py +5 -11
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/PKG-INFO +18 -1
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/SOURCES.txt +1 -0
- mutmut-3.3.1/tests/test_generation_error_handling.py +35 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/test_mutation.py +68 -55
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/test_mutmut3.py +3 -6
- {mutmut-3.3.0 → mutmut-3.3.1}/AUTHORS.rst +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/LICENSE +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/MANIFEST.in +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/file_mutation.cpython-312.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/node_mutation.cpython-312.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/trampoline_templates.cpython-312.pyc +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/result_browser_layout.tcss +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/dependency_links.txt +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/entry_points.txt +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/not-zip-safe +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/requires.txt +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/top_level.txt +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/requirements.txt +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/setup.cfg +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/setup.py +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/__init__.py +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/__init__.py +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/test_cli_version.py +0 -0
- {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/test_e2e_result_snapshots.py +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
|
+
3.3.1
|
|
5
|
+
~~~~~
|
|
6
|
+
|
|
7
|
+
* Increased threshold for mutant timeouts
|
|
8
|
+
|
|
9
|
+
* Added `tests_dir` config. Accepts a single entry or a list of directories.
|
|
10
|
+
|
|
11
|
+
* Async generators fixes
|
|
12
|
+
|
|
13
|
+
* Fixed bad mutations for certain string escape sequences
|
|
14
|
+
|
|
15
|
+
* Performance fixes
|
|
16
|
+
|
|
17
|
+
* Various internal bug fixes
|
|
18
|
+
|
|
19
|
+
|
|
4
20
|
3.3.0
|
|
5
21
|
~~~~~
|
|
6
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: mutation testing for Python 3
|
|
5
5
|
Home-page: https://github.com/boxed/mutmut
|
|
6
6
|
Author: Anders Hovmöller
|
|
@@ -88,6 +88,9 @@ You can also write a mutant to disk from the `browse` interface, or via
|
|
|
88
88
|
source code control and committed before you apply a mutant!
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
|
|
91
94
|
Configuration
|
|
92
95
|
-------------
|
|
93
96
|
|
|
@@ -173,6 +176,20 @@ You can exclude files from mutation in `setup.cfg`:
|
|
|
173
176
|
*__tests.py
|
|
174
177
|
|
|
175
178
|
|
|
179
|
+
Enable debug output (increase verbosity)
|
|
180
|
+
----------------------------------------
|
|
181
|
+
|
|
182
|
+
By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
|
|
183
|
+
|
|
184
|
+
If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
|
|
185
|
+
Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
|
|
186
|
+
to failing tests.
|
|
187
|
+
|
|
188
|
+
.. code-block::
|
|
189
|
+
|
|
190
|
+
debug=true
|
|
191
|
+
|
|
192
|
+
|
|
176
193
|
Whitelisting
|
|
177
194
|
------------
|
|
178
195
|
|
|
@@ -64,6 +64,9 @@ You can also write a mutant to disk from the `browse` interface, or via
|
|
|
64
64
|
source code control and committed before you apply a mutant!
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
|
|
67
70
|
Configuration
|
|
68
71
|
-------------
|
|
69
72
|
|
|
@@ -149,6 +152,20 @@ You can exclude files from mutation in `setup.cfg`:
|
|
|
149
152
|
*__tests.py
|
|
150
153
|
|
|
151
154
|
|
|
155
|
+
Enable debug output (increase verbosity)
|
|
156
|
+
----------------------------------------
|
|
157
|
+
|
|
158
|
+
By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
|
|
159
|
+
|
|
160
|
+
If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
|
|
161
|
+
Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
|
|
162
|
+
to failing tests.
|
|
163
|
+
|
|
164
|
+
.. code-block::
|
|
165
|
+
|
|
166
|
+
debug=true
|
|
167
|
+
|
|
168
|
+
|
|
152
169
|
Whitelisting
|
|
153
170
|
------------
|
|
154
171
|
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ import gc
|
|
|
4
4
|
import inspect
|
|
5
5
|
import itertools
|
|
6
6
|
import json
|
|
7
|
-
from multiprocessing import Pool, set_start_method
|
|
7
|
+
from multiprocessing import Pool, set_start_method, Lock
|
|
8
8
|
import os
|
|
9
9
|
import resource
|
|
10
10
|
import shutil
|
|
@@ -47,7 +47,9 @@ from typing import (
|
|
|
47
47
|
Dict,
|
|
48
48
|
List,
|
|
49
49
|
Union,
|
|
50
|
+
Optional,
|
|
50
51
|
)
|
|
52
|
+
import warnings
|
|
51
53
|
|
|
52
54
|
import click
|
|
53
55
|
import libcst as cst
|
|
@@ -77,6 +79,7 @@ status_by_exit_code = {
|
|
|
77
79
|
34: 'skipped',
|
|
78
80
|
35: 'suspicious',
|
|
79
81
|
36: 'timeout',
|
|
82
|
+
-24: 'timeout', # SIGXCPU
|
|
80
83
|
24: 'timeout', # SIGXCPU
|
|
81
84
|
152: 'timeout', # SIGXCPU
|
|
82
85
|
255: 'timeout',
|
|
@@ -176,6 +179,13 @@ class BadTestExecutionCommandsException(Exception):
|
|
|
176
179
|
super().__init__(msg)
|
|
177
180
|
|
|
178
181
|
|
|
182
|
+
class InvalidGeneratedSyntaxException(Exception):
|
|
183
|
+
def __init__(self, file: Union[Path, str]) -> None:
|
|
184
|
+
super().__init__(f'Mutmut generated invalid python syntax for {file}. '
|
|
185
|
+
'If the original file has valid python syntax, please file an issue '
|
|
186
|
+
'with a minimal reproducible example file.')
|
|
187
|
+
|
|
188
|
+
|
|
179
189
|
def copy_src_dir():
|
|
180
190
|
for path in mutmut.config.paths_to_mutate:
|
|
181
191
|
output_path: Path = Path('mutants') / path
|
|
@@ -185,21 +195,33 @@ def copy_src_dir():
|
|
|
185
195
|
output_path.parent.mkdir(exist_ok=True, parents=True)
|
|
186
196
|
shutil.copyfile(path, output_path)
|
|
187
197
|
|
|
198
|
+
@dataclass
|
|
199
|
+
class FileMutationResult:
|
|
200
|
+
"""Dataclass to transfer warnings and errors from child processes to the parent"""
|
|
201
|
+
warnings: list[Warning]
|
|
202
|
+
error: Optional[Exception] = None
|
|
188
203
|
|
|
189
204
|
def create_mutants(max_children: int):
|
|
190
205
|
with Pool(processes=max_children) as p:
|
|
191
|
-
p.
|
|
192
|
-
|
|
206
|
+
for result in p.imap_unordered(create_file_mutants, walk_source_files()):
|
|
207
|
+
for warning in result.warnings:
|
|
208
|
+
warnings.warn(warning)
|
|
209
|
+
if result.error:
|
|
210
|
+
raise result.error
|
|
193
211
|
|
|
194
|
-
def create_file_mutants(path: Path):
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
def create_file_mutants(path: Path) -> FileMutationResult:
|
|
213
|
+
try:
|
|
214
|
+
print(path)
|
|
215
|
+
output_path = Path('mutants') / path
|
|
216
|
+
makedirs(output_path.parent, exist_ok=True)
|
|
198
217
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
if mutmut.config.should_ignore_for_mutation(path):
|
|
219
|
+
shutil.copy(path, output_path)
|
|
220
|
+
return FileMutationResult(warnings=[])
|
|
221
|
+
else:
|
|
222
|
+
return create_mutants_for_file(path, output_path)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
return FileMutationResult(warnings=[], error=e)
|
|
203
225
|
|
|
204
226
|
|
|
205
227
|
def copy_also_copy_files():
|
|
@@ -215,23 +237,30 @@ def copy_also_copy_files():
|
|
|
215
237
|
else:
|
|
216
238
|
shutil.copytree(path, destination, dirs_exist_ok=True)
|
|
217
239
|
|
|
218
|
-
|
|
219
|
-
def create_mutants_for_file(filename, output_path):
|
|
240
|
+
def create_mutants_for_file(filename, output_path) -> FileMutationResult:
|
|
220
241
|
input_stat = os.stat(filename)
|
|
242
|
+
warnings: list[Warning] = []
|
|
221
243
|
|
|
222
244
|
with open(filename) as f:
|
|
223
245
|
source = f.read()
|
|
224
246
|
|
|
225
247
|
with open(output_path, 'w') as out:
|
|
226
|
-
|
|
248
|
+
try:
|
|
249
|
+
mutant_names, hash_by_function_name = write_all_mutants_to_file(out=out, source=source, filename=filename)
|
|
250
|
+
except cst.ParserSyntaxError as e:
|
|
251
|
+
# if libcst cannot parse it, then copy the source without any mutations
|
|
252
|
+
warnings.append(SyntaxWarning(f'Unsupported syntax in {filename} ({str(e)}), skipping'))
|
|
253
|
+
out.write(source)
|
|
254
|
+
mutant_names, hash_by_function_name = [], {}
|
|
227
255
|
|
|
228
256
|
# validate no syntax errors of mutants
|
|
229
257
|
with open(output_path) as f:
|
|
230
258
|
try:
|
|
231
259
|
ast.parse(f.read())
|
|
232
260
|
except (IndentationError, SyntaxError) as e:
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
invalid_syntax_error = InvalidGeneratedSyntaxException(output_path)
|
|
262
|
+
invalid_syntax_error.__cause__ = e
|
|
263
|
+
return FileMutationResult(warnings=warnings, error=invalid_syntax_error)
|
|
235
264
|
|
|
236
265
|
source_file_mutation_data = SourceFileMutationData(path=filename)
|
|
237
266
|
module_name = strip_prefix(str(filename)[:-len(filename.suffix)].replace(os.sep, '.'), prefix='src.')
|
|
@@ -245,6 +274,7 @@ def create_mutants_for_file(filename, output_path):
|
|
|
245
274
|
source_file_mutation_data.save()
|
|
246
275
|
|
|
247
276
|
os.utime(output_path, (input_stat.st_atime, input_stat.st_mtime))
|
|
277
|
+
return FileMutationResult(warnings=warnings)
|
|
248
278
|
|
|
249
279
|
|
|
250
280
|
def write_all_mutants_to_file(*, out, source, filename):
|
|
@@ -282,7 +312,8 @@ class SourceFileMutationData:
|
|
|
282
312
|
|
|
283
313
|
def register_pid(self, *, pid, key, estimated_time_of_tests):
|
|
284
314
|
self.key_by_pid[pid] = key
|
|
285
|
-
|
|
315
|
+
with START_TIMES_BY_PID_LOCK:
|
|
316
|
+
self.start_time_by_pid[pid] = datetime.now()
|
|
286
317
|
self.estimated_time_of_tests_by_pid[pid] = estimated_time_of_tests
|
|
287
318
|
|
|
288
319
|
def register_result(self, *, pid, exit_code):
|
|
@@ -290,7 +321,8 @@ class SourceFileMutationData:
|
|
|
290
321
|
self.exit_code_by_key[self.key_by_pid[pid]] = exit_code
|
|
291
322
|
# TODO: maybe rate limit this? Saving on each result can slow down mutation testing a lot if the test run is fast.
|
|
292
323
|
del self.key_by_pid[pid]
|
|
293
|
-
|
|
324
|
+
with START_TIMES_BY_PID_LOCK:
|
|
325
|
+
del self.start_time_by_pid[pid]
|
|
294
326
|
self.save()
|
|
295
327
|
|
|
296
328
|
def stop_children(self):
|
|
@@ -397,16 +429,34 @@ class PytestRunner(TestRunner):
|
|
|
397
429
|
|
|
398
430
|
stats_collector = StatsCollector()
|
|
399
431
|
|
|
432
|
+
pytest_args = ['-x', '-q']
|
|
433
|
+
if tests:
|
|
434
|
+
pytest_args += list(tests)
|
|
435
|
+
else:
|
|
436
|
+
tests_dir = mutmut.config.tests_dir
|
|
437
|
+
if tests_dir:
|
|
438
|
+
pytest_args += tests_dir
|
|
400
439
|
with change_cwd('mutants'):
|
|
401
|
-
return int(self.execute_pytest(
|
|
440
|
+
return int(self.execute_pytest(pytest_args, plugins=[stats_collector]))
|
|
402
441
|
|
|
403
442
|
def run_tests(self, *, mutant_name, tests):
|
|
443
|
+
pytest_args = ['-x', '-q']
|
|
444
|
+
if tests:
|
|
445
|
+
pytest_args += list(tests)
|
|
446
|
+
else:
|
|
447
|
+
tests_dir = mutmut.config.tests_dir
|
|
448
|
+
if tests_dir:
|
|
449
|
+
pytest_args += tests_dir
|
|
404
450
|
with change_cwd('mutants'):
|
|
405
|
-
return int(self.execute_pytest(
|
|
451
|
+
return int(self.execute_pytest(pytest_args))
|
|
406
452
|
|
|
407
453
|
def run_forced_fail(self):
|
|
454
|
+
pytest_args = ['-x', '-q']
|
|
455
|
+
tests_dir = mutmut.config.tests_dir
|
|
456
|
+
if tests_dir:
|
|
457
|
+
pytest_args += tests_dir
|
|
408
458
|
with change_cwd('mutants'):
|
|
409
|
-
return int(self.execute_pytest(
|
|
459
|
+
return int(self.execute_pytest(pytest_args))
|
|
410
460
|
|
|
411
461
|
def list_all_tests(self):
|
|
412
462
|
class TestsCollector:
|
|
@@ -415,8 +465,13 @@ class PytestRunner(TestRunner):
|
|
|
415
465
|
|
|
416
466
|
collector = TestsCollector()
|
|
417
467
|
|
|
468
|
+
tests_dir = mutmut.config.tests_dir
|
|
469
|
+
pytest_args = ['-x', '-q', '--collect-only']
|
|
470
|
+
if tests_dir:
|
|
471
|
+
pytest_args += tests_dir
|
|
472
|
+
|
|
418
473
|
with change_cwd('mutants'):
|
|
419
|
-
exit_code = int(self.execute_pytest(
|
|
474
|
+
exit_code = int(self.execute_pytest(pytest_args, plugins=[collector]))
|
|
420
475
|
if exit_code != 0:
|
|
421
476
|
raise CollectTestsFailedException()
|
|
422
477
|
|
|
@@ -620,6 +675,7 @@ class Config:
|
|
|
620
675
|
max_stack_depth: int
|
|
621
676
|
debug: bool
|
|
622
677
|
paths_to_mutate: List[Path]
|
|
678
|
+
tests_dir: List[str] = None
|
|
623
679
|
|
|
624
680
|
def should_ignore_for_mutation(self, path):
|
|
625
681
|
if not str(path).endswith('.py'):
|
|
@@ -698,7 +754,8 @@ def load_config():
|
|
|
698
754
|
paths_to_mutate=[
|
|
699
755
|
Path(y)
|
|
700
756
|
for y in s('paths_to_mutate', [])
|
|
701
|
-
] or guess_paths_to_mutate()
|
|
757
|
+
] or guess_paths_to_mutate(),
|
|
758
|
+
tests_dir=s('tests_dir', []),
|
|
702
759
|
)
|
|
703
760
|
|
|
704
761
|
|
|
@@ -860,6 +917,9 @@ def stop_all_children(mutants):
|
|
|
860
917
|
for m, _, _ in mutants:
|
|
861
918
|
m.stop_children()
|
|
862
919
|
|
|
920
|
+
# used to copy the global mutmut.config to subprocesses
|
|
921
|
+
set_start_method('fork')
|
|
922
|
+
START_TIMES_BY_PID_LOCK = Lock()
|
|
863
923
|
|
|
864
924
|
def timeout_checker(mutants):
|
|
865
925
|
def inner_timout_checker():
|
|
@@ -868,9 +928,12 @@ def timeout_checker(mutants):
|
|
|
868
928
|
|
|
869
929
|
now = datetime.now()
|
|
870
930
|
for m, mutant_name, result in mutants:
|
|
871
|
-
|
|
931
|
+
# copy dict inside lock, so it is not modified by another process while we iterate it
|
|
932
|
+
with START_TIMES_BY_PID_LOCK:
|
|
933
|
+
start_times_by_pid = dict(m.start_time_by_pid)
|
|
934
|
+
for pid, start_time in start_times_by_pid.items():
|
|
872
935
|
run_time = now - start_time
|
|
873
|
-
if run_time.total_seconds() > (m.estimated_time_of_tests_by_mutant[mutant_name] + 1) *
|
|
936
|
+
if run_time.total_seconds() > (m.estimated_time_of_tests_by_mutant[mutant_name] + 1) * 15:
|
|
874
937
|
try:
|
|
875
938
|
os.kill(pid, signal.SIGXCPU)
|
|
876
939
|
except ProcessLookupError:
|
|
@@ -882,9 +945,6 @@ def timeout_checker(mutants):
|
|
|
882
945
|
@click.option('--max-children', type=int)
|
|
883
946
|
@click.argument('mutant_names', required=False, nargs=-1)
|
|
884
947
|
def run(mutant_names, *, max_children):
|
|
885
|
-
# used to copy the global mutmut.config to subprocesses
|
|
886
|
-
set_start_method('fork')
|
|
887
|
-
|
|
888
948
|
assert isinstance(mutant_names, (tuple, list)), mutant_names
|
|
889
949
|
_run(mutant_names, max_children)
|
|
890
950
|
|
|
@@ -1007,8 +1067,9 @@ def _run(mutant_names: Union[tuple, list], max_children: Union[None, int]):
|
|
|
1007
1067
|
os._exit(33)
|
|
1008
1068
|
|
|
1009
1069
|
estimated_time_of_tests = m.estimated_time_of_tests_by_mutant[mutant_name]
|
|
1010
|
-
cpu_time_limit = ceil((estimated_time_of_tests + 1) *
|
|
1011
|
-
|
|
1070
|
+
cpu_time_limit = ceil((estimated_time_of_tests + 1) * 30 + process_time())
|
|
1071
|
+
# signal SIGXCPU after <cpu_time_limit>. One second later signal SIGKILL if it is still running
|
|
1072
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit + 1))
|
|
1012
1073
|
|
|
1013
1074
|
with CatchOutput():
|
|
1014
1075
|
result = runner.run_tests(mutant_name=mutant_name, tests=tests)
|
|
@@ -1098,16 +1159,24 @@ def read_orig_module(path) -> cst.Module:
|
|
|
1098
1159
|
return cst.parse_module(f.read())
|
|
1099
1160
|
|
|
1100
1161
|
|
|
1101
|
-
def
|
|
1162
|
+
def find_top_level_function_or_method(module: cst.Module, name: str) -> Union[cst.FunctionDef, None]:
|
|
1102
1163
|
name = name.split('.')[-1]
|
|
1103
|
-
|
|
1164
|
+
for child in module.body:
|
|
1165
|
+
if isinstance(child, cst.FunctionDef) and child.name.value == name:
|
|
1166
|
+
return child
|
|
1167
|
+
if isinstance(child, cst.ClassDef) and isinstance(child.body, cst.IndentedBlock):
|
|
1168
|
+
for method in child.body.body:
|
|
1169
|
+
if isinstance(method, cst.FunctionDef) and method.name.value == name:
|
|
1170
|
+
return method
|
|
1171
|
+
|
|
1172
|
+
return None
|
|
1104
1173
|
|
|
1105
1174
|
|
|
1106
1175
|
def read_original_function(module: cst.Module, mutant_name: str):
|
|
1107
1176
|
orig_function_name, _ = orig_function_and_class_names_from_key(mutant_name)
|
|
1108
1177
|
orig_name = mangled_name_from_mutant_name(mutant_name) + '__mutmut_orig'
|
|
1109
1178
|
|
|
1110
|
-
result =
|
|
1179
|
+
result = find_top_level_function_or_method(module, orig_name)
|
|
1111
1180
|
if not result:
|
|
1112
1181
|
raise FileNotFoundError(f'Could not find original function "{orig_function_name}"')
|
|
1113
1182
|
return result.with_changes(name = cst.Name(orig_function_name))
|
|
@@ -1116,7 +1185,7 @@ def read_original_function(module: cst.Module, mutant_name: str):
|
|
|
1116
1185
|
def read_mutant_function(module: cst.Module, mutant_name: str):
|
|
1117
1186
|
orig_function_name, _ = orig_function_and_class_names_from_key(mutant_name)
|
|
1118
1187
|
|
|
1119
|
-
result =
|
|
1188
|
+
result = find_top_level_function_or_method(module, mutant_name)
|
|
1120
1189
|
if not result:
|
|
1121
1190
|
raise FileNotFoundError(f'Could not find original function "{orig_function_name}"')
|
|
1122
1191
|
return result.with_changes(name = cst.Name(orig_function_name))
|
|
@@ -1189,7 +1258,7 @@ def apply_mutant(mutant_name):
|
|
|
1189
1258
|
mutant_function = read_mutant_function(mutants_module, mutant_name)
|
|
1190
1259
|
mutant_function = mutant_function.with_changes(name=cst.Name(orig_function_name))
|
|
1191
1260
|
|
|
1192
|
-
original_function =
|
|
1261
|
+
original_function = find_top_level_function_or_method(orig_module, orig_function_name)
|
|
1193
1262
|
if not original_function:
|
|
1194
1263
|
raise FileNotFoundError(f'Could not apply mutant {mutant_name}')
|
|
1195
1264
|
|
|
@@ -1263,6 +1332,7 @@ def browse(show_killed):
|
|
|
1263
1332
|
def read_data(self):
|
|
1264
1333
|
ensure_config_loaded()
|
|
1265
1334
|
self.source_file_mutation_data_and_stat_by_path = {}
|
|
1335
|
+
self.path_by_name = {}
|
|
1266
1336
|
|
|
1267
1337
|
for p in walk_source_files():
|
|
1268
1338
|
if mutmut.config.should_ignore_for_mutation(p):
|
|
@@ -1272,6 +1342,8 @@ def browse(show_killed):
|
|
|
1272
1342
|
stat = collect_stat(source_file_mutation_data)
|
|
1273
1343
|
|
|
1274
1344
|
self.source_file_mutation_data_and_stat_by_path[str(p)] = source_file_mutation_data, stat
|
|
1345
|
+
for name in source_file_mutation_data.exit_code_by_key:
|
|
1346
|
+
self.path_by_name[name] = p
|
|
1275
1347
|
|
|
1276
1348
|
def populate_files_table(self):
|
|
1277
1349
|
# noinspection PyTypeChecker
|
|
@@ -1310,11 +1382,12 @@ def browse(show_killed):
|
|
|
1310
1382
|
else:
|
|
1311
1383
|
diff_view.update('<loading...>')
|
|
1312
1384
|
self.loading_id = event.row_key.value
|
|
1385
|
+
path = self.path_by_name.get(event.row_key.value)
|
|
1313
1386
|
|
|
1314
1387
|
def load_thread():
|
|
1315
1388
|
ensure_config_loaded()
|
|
1316
1389
|
try:
|
|
1317
|
-
d = get_diff_for_mutant(event.row_key.value)
|
|
1390
|
+
d = get_diff_for_mutant(event.row_key.value, path=path)
|
|
1318
1391
|
if event.row_key.value == self.loading_id:
|
|
1319
1392
|
diff_view.update(Syntax(d, "diff"))
|
|
1320
1393
|
except Exception as e:
|
|
@@ -8,7 +8,7 @@ import warnings
|
|
|
8
8
|
import libcst as cst
|
|
9
9
|
from libcst.metadata import PositionProvider, MetadataWrapper
|
|
10
10
|
import libcst.matchers as m
|
|
11
|
-
from mutmut.trampoline_templates import build_trampoline, mangle_function_name, trampoline_impl
|
|
11
|
+
from mutmut.trampoline_templates import build_trampoline, mangle_function_name, trampoline_impl
|
|
12
12
|
from mutmut.node_mutation import mutation_operators, OPERATORS_TYPE
|
|
13
13
|
|
|
14
14
|
NEVER_MUTATE_FUNCTION_NAMES = { "__getattribute__", "__setattr__", "__new__" }
|
|
@@ -25,11 +25,7 @@ def mutate_file_contents(filename: str, code: str) -> tuple[str, Sequence[str]]:
|
|
|
25
25
|
"""Create mutations for `code` and merge them to a single mutated file with trampolines.
|
|
26
26
|
|
|
27
27
|
:return: A tuple of (mutated code, list of mutant function names)"""
|
|
28
|
-
|
|
29
|
-
module, mutations = create_mutations(code)
|
|
30
|
-
except cst.ParserSyntaxError as e:
|
|
31
|
-
warnings.warn(SyntaxWarning(f'Unsupported syntax in {filename} ({str(e)}), skipping'))
|
|
32
|
-
return code, []
|
|
28
|
+
module, mutations = create_mutations(code)
|
|
33
29
|
|
|
34
30
|
return combine_mutations_to_source(module, mutations)
|
|
35
31
|
|
|
@@ -165,8 +161,6 @@ MODULE_STATEMENT = Union[cst.SimpleStatementLine, cst.BaseCompoundStatement]
|
|
|
165
161
|
# convert str trampoline implementations to CST nodes with some whitespace
|
|
166
162
|
trampoline_impl_cst = list(cst.parse_module(trampoline_impl).body)
|
|
167
163
|
trampoline_impl_cst[-1] = trampoline_impl_cst[-1].with_changes(leading_lines = [cst.EmptyLine(), cst.EmptyLine()])
|
|
168
|
-
yield_from_trampoline_impl_cst = list(cst.parse_module(yield_from_trampoline_impl).body)
|
|
169
|
-
yield_from_trampoline_impl_cst[-1] = yield_from_trampoline_impl_cst[-1].with_changes(leading_lines = [cst.EmptyLine(), cst.EmptyLine()])
|
|
170
164
|
|
|
171
165
|
|
|
172
166
|
def combine_mutations_to_source(module: cst.Module, mutations: Sequence[Mutation]) -> tuple[str, Sequence[str]]:
|
|
@@ -185,7 +179,6 @@ def combine_mutations_to_source(module: cst.Module, mutations: Sequence[Mutation
|
|
|
185
179
|
|
|
186
180
|
# trampoline functions
|
|
187
181
|
result.extend(trampoline_impl_cst)
|
|
188
|
-
result.extend(yield_from_trampoline_impl_cst)
|
|
189
182
|
|
|
190
183
|
mutations_within_function = group_by_top_level_node(mutations)
|
|
191
184
|
|
|
@@ -234,7 +227,6 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable
|
|
|
234
227
|
|
|
235
228
|
name = function.name.value
|
|
236
229
|
mangled_name = mangle_function_name(name=name, class_name=class_name) + '__mutmut'
|
|
237
|
-
_is_generator = is_generator(function)
|
|
238
230
|
|
|
239
231
|
# copy of original function
|
|
240
232
|
nodes.append(function.with_changes(name=cst.Name(mangled_name + '_orig')))
|
|
@@ -248,7 +240,7 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable
|
|
|
248
240
|
nodes.append(mutated_method) # type: ignore
|
|
249
241
|
|
|
250
242
|
# trampoline that forwards the calls
|
|
251
|
-
trampoline = list(cst.parse_module(build_trampoline(orig_name=name, mutants=mutant_names, class_name=class_name
|
|
243
|
+
trampoline = list(cst.parse_module(build_trampoline(orig_name=name, mutants=mutant_names, class_name=class_name)).body)
|
|
252
244
|
trampoline[0] = trampoline[0].with_changes(leading_lines=[cst.EmptyLine()])
|
|
253
245
|
nodes.extend(trampoline)
|
|
254
246
|
|
|
@@ -274,28 +266,6 @@ def group_by_top_level_node(mutations: Sequence[Mutation]) -> Mapping[cst.CSTNod
|
|
|
274
266
|
|
|
275
267
|
return grouped
|
|
276
268
|
|
|
277
|
-
def is_generator(function: cst.FunctionDef) -> bool:
|
|
278
|
-
"""Return True if the function has yield statement(s)."""
|
|
279
|
-
visitor = IsGeneratorVisitor(function)
|
|
280
|
-
function.visit(visitor)
|
|
281
|
-
return visitor.is_generator
|
|
282
|
-
|
|
283
|
-
class IsGeneratorVisitor(cst.CSTVisitor):
|
|
284
|
-
"""Check if a function is a generator.
|
|
285
|
-
We do so by checking if any child is a Yield statement, but not looking into inner function definitions."""
|
|
286
|
-
def __init__(self, original_function: cst.FunctionDef):
|
|
287
|
-
self.is_generator = False
|
|
288
|
-
self.original_function: cst.FunctionDef = original_function
|
|
289
|
-
|
|
290
|
-
def visit_FunctionDef(self, node):
|
|
291
|
-
# do not recurse into inner function definitions
|
|
292
|
-
if self.original_function != node:
|
|
293
|
-
return False
|
|
294
|
-
|
|
295
|
-
def visit_Yield(self, node):
|
|
296
|
-
self.is_generator = True
|
|
297
|
-
return False
|
|
298
|
-
|
|
299
269
|
def pragma_no_mutate_lines(source: str) -> set[int]:
|
|
300
270
|
return {
|
|
301
271
|
i + 1
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""This module contains the mutations for indidvidual nodes, e.g. replacing a != b with a == b."""
|
|
2
|
-
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Union, cast
|
|
3
4
|
from collections.abc import Callable, Iterable, Sequence
|
|
4
5
|
import libcst as cst
|
|
5
6
|
import libcst.matchers as m
|
|
@@ -11,6 +12,9 @@ OPERATORS_TYPE = Sequence[
|
|
|
11
12
|
]
|
|
12
13
|
]
|
|
13
14
|
|
|
15
|
+
# pattern to match (nearly) all chars in a string that are not part of an escape sequence
|
|
16
|
+
NON_ESCAPE_SEQUENCE = re.compile(r"((?<!\\)[^\\]+)")
|
|
17
|
+
|
|
14
18
|
def operator_number(
|
|
15
19
|
node: cst.BaseNumber
|
|
16
20
|
) -> Iterable[cst.BaseNumber]:
|
|
@@ -39,9 +43,9 @@ def operator_string(
|
|
|
39
43
|
|
|
40
44
|
supported_str_mutations: list[Callable[[str], str]] = [
|
|
41
45
|
lambda x: "XX" + x + "XX",
|
|
42
|
-
|
|
43
|
-
lambda x:
|
|
44
|
-
lambda x:
|
|
46
|
+
# do not modify escape sequences, as this could break python syntax
|
|
47
|
+
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).lower(), x),
|
|
48
|
+
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).upper(), x),
|
|
45
49
|
]
|
|
46
50
|
|
|
47
51
|
for mut_func in supported_str_mutations:
|
|
@@ -96,7 +100,7 @@ def operator_arg_removal(
|
|
|
96
100
|
yield node.with_changes(args=[*node.args[:i], *node.args[i + 1 :]])
|
|
97
101
|
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
supported_symmetric_str_methods_swap = [
|
|
100
104
|
("lower", "upper"),
|
|
101
105
|
("upper", "lower"),
|
|
102
106
|
("lstrip", "rstrip"),
|
|
@@ -107,24 +111,42 @@ supported_str_methods_swap = [
|
|
|
107
111
|
("rjust", "ljust"),
|
|
108
112
|
("index", "rindex"),
|
|
109
113
|
("rindex", "index"),
|
|
110
|
-
("split", "rsplit"),
|
|
111
|
-
("rsplit", "split"),
|
|
112
114
|
("removeprefix", "removesuffix"),
|
|
113
115
|
("removesuffix", "removeprefix"),
|
|
114
116
|
("partition", "rpartition"),
|
|
115
117
|
("rpartition", "partition")
|
|
116
|
-
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
supported_unsymmetrical_str_methods_swap = [
|
|
121
|
+
("split", "rsplit"),
|
|
122
|
+
("rsplit", "split")
|
|
123
|
+
]
|
|
117
124
|
|
|
118
|
-
def
|
|
125
|
+
def operator_symmetric_string_methods_swap(
|
|
119
126
|
node: cst.Call
|
|
120
127
|
) -> Iterable[cst.Call]:
|
|
121
128
|
"""try to swap string method to opposite e.g. a.lower() -> a.upper()"""
|
|
122
129
|
|
|
123
|
-
for old_call, new_call in
|
|
130
|
+
for old_call, new_call in supported_symmetric_str_methods_swap:
|
|
124
131
|
if m.matches(node.func, m.Attribute(value=m.DoNotCare(), attr=m.Name(value=old_call))):
|
|
125
132
|
func_name = cst.ensure_type(node.func, cst.Attribute).attr
|
|
126
133
|
yield node.with_deep_changes(func_name, value=new_call)
|
|
127
134
|
|
|
135
|
+
def operator_unsymmetrical_string_methods_swap(
|
|
136
|
+
node: cst.Call
|
|
137
|
+
) -> Iterable[cst.Call]:
|
|
138
|
+
"""Try to handle specific mutations of string, which useful only in specific args combination."""
|
|
139
|
+
for old_call, new_call in supported_unsymmetrical_str_methods_swap:
|
|
140
|
+
if m.matches(node.func, m.Attribute(attr=m.Name(value=old_call))):
|
|
141
|
+
if old_call in {"split", "rsplit"}:
|
|
142
|
+
# The logic of this "if" operator described here:
|
|
143
|
+
# https://github.com/boxed/mutmut/pull/394#issuecomment-2977890188
|
|
144
|
+
key_args: set[str] = {a.keyword.value for a in node.args if a.keyword} # sep or maxsplit or nothing
|
|
145
|
+
if len(node.args) == 2 or "maxsplit" in key_args:
|
|
146
|
+
func_name = cst.ensure_type(node.func, cst.Attribute).attr
|
|
147
|
+
yield node.with_deep_changes(func_name, value=new_call)
|
|
148
|
+
|
|
149
|
+
|
|
128
150
|
|
|
129
151
|
def operator_remove_unary_ops(
|
|
130
152
|
node: cst.UnaryOperation
|
|
@@ -197,7 +219,11 @@ _operator_mapping: dict[type[cst.CSTNode], type[cst.CSTNode]] = {
|
|
|
197
219
|
def operator_swap_op(
|
|
198
220
|
node: cst.CSTNode
|
|
199
221
|
) -> Iterable[cst.CSTNode]:
|
|
200
|
-
|
|
222
|
+
if m.matches(node, m.BinaryOperation() | m.UnaryOperation() | m.BooleanOperation() | m.ComparisonTarget() | m.AugAssign()):
|
|
223
|
+
typed_node = cast(Union[cst.BinaryOperation, cst.UnaryOperation, cst.BooleanOperation, cst.ComparisonTarget, cst.AugAssign], node)
|
|
224
|
+
operator = typed_node.operator
|
|
225
|
+
for new_operator in _simple_mutation_mapping(operator, _operator_mapping):
|
|
226
|
+
yield node.with_changes(operator=new_operator)
|
|
201
227
|
|
|
202
228
|
|
|
203
229
|
def operator_augmented_assignment(
|
|
@@ -238,7 +264,8 @@ mutation_operators: OPERATORS_TYPE = [
|
|
|
238
264
|
(cst.UnaryOperation, operator_remove_unary_ops),
|
|
239
265
|
(cst.Call, operator_dict_arguments),
|
|
240
266
|
(cst.Call, operator_arg_removal),
|
|
241
|
-
(cst.Call,
|
|
267
|
+
(cst.Call, operator_symmetric_string_methods_swap),
|
|
268
|
+
(cst.Call, operator_unsymmetrical_string_methods_swap),
|
|
242
269
|
(cst.Lambda, operator_lambda),
|
|
243
270
|
(cst.CSTNode, operator_keywords),
|
|
244
271
|
(cst.CSTNode, operator_swap_op),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
CLASS_NAME_SEPARATOR = 'ǁ'
|
|
2
2
|
|
|
3
|
-
def build_trampoline(*, orig_name, mutants, class_name
|
|
3
|
+
def build_trampoline(*, orig_name, mutants, class_name):
|
|
4
4
|
mangled_name = mangle_function_name(name=orig_name, class_name=class_name)
|
|
5
5
|
|
|
6
6
|
mutants_dict = f'{mangled_name}__mutmut_mutants : ClassVar[MutantDict] = {{\n' + ', \n '.join(f'{repr(m)}: {m}' for m in mutants) + '\n}'
|
|
@@ -12,18 +12,13 @@ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
|
|
|
12
12
|
access_suffix = '")'
|
|
13
13
|
self_arg = ', self'
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
yield_statement = 'yield from ' # note the space at the end!
|
|
17
|
-
trampoline_name = '_mutmut_yield_from_trampoline'
|
|
18
|
-
else:
|
|
19
|
-
yield_statement = ''
|
|
20
|
-
trampoline_name = '_mutmut_trampoline'
|
|
15
|
+
trampoline_name = '_mutmut_trampoline'
|
|
21
16
|
|
|
22
17
|
return f"""
|
|
23
18
|
{mutants_dict}
|
|
24
19
|
|
|
25
20
|
def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
|
|
26
|
-
result = {
|
|
21
|
+
result = {trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, args, kwargs{self_arg})
|
|
27
22
|
return result
|
|
28
23
|
|
|
29
24
|
{orig_name}.__signature__ = _mutmut_signature({mangled_name}__mutmut_orig)
|
|
@@ -62,11 +57,11 @@ def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None):
|
|
|
62
57
|
from mutmut.__main__ import record_trampoline_hit
|
|
63
58
|
record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
|
|
64
59
|
result = orig(*call_args, **call_kwargs)
|
|
65
|
-
return result
|
|
60
|
+
return result
|
|
66
61
|
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
|
|
67
62
|
if not mutant_under_test.startswith(prefix):
|
|
68
63
|
result = orig(*call_args, **call_kwargs)
|
|
69
|
-
return result
|
|
64
|
+
return result
|
|
70
65
|
mutant_name = mutant_under_test.rpartition('.')[-1]
|
|
71
66
|
if self_arg:
|
|
72
67
|
# call to a class method where self is not bound
|
|
@@ -76,4 +71,3 @@ def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None):
|
|
|
76
71
|
return result
|
|
77
72
|
|
|
78
73
|
"""
|
|
79
|
-
yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: mutation testing for Python 3
|
|
5
5
|
Home-page: https://github.com/boxed/mutmut
|
|
6
6
|
Author: Anders Hovmöller
|
|
@@ -88,6 +88,9 @@ You can also write a mutant to disk from the `browse` interface, or via
|
|
|
88
88
|
source code control and committed before you apply a mutant!
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
|
|
91
94
|
Configuration
|
|
92
95
|
-------------
|
|
93
96
|
|
|
@@ -173,6 +176,20 @@ You can exclude files from mutation in `setup.cfg`:
|
|
|
173
176
|
*__tests.py
|
|
174
177
|
|
|
175
178
|
|
|
179
|
+
Enable debug output (increase verbosity)
|
|
180
|
+
----------------------------------------
|
|
181
|
+
|
|
182
|
+
By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
|
|
183
|
+
|
|
184
|
+
If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
|
|
185
|
+
Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
|
|
186
|
+
to failing tests.
|
|
187
|
+
|
|
188
|
+
.. code-block::
|
|
189
|
+
|
|
190
|
+
debug=true
|
|
191
|
+
|
|
192
|
+
|
|
176
193
|
Whitelisting
|
|
177
194
|
------------
|
|
178
195
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from mutmut.__main__ import create_mutants, Config, InvalidGeneratedSyntaxException
|
|
3
|
+
import mutmut.__main__
|
|
4
|
+
import mutmut
|
|
5
|
+
import pytest
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
source_dir = Path(__file__).parent / 'data' / 'test_generation'
|
|
9
|
+
source_dir = source_dir.relative_to(os.getcwd())
|
|
10
|
+
|
|
11
|
+
class MockConfig:
|
|
12
|
+
def should_ignore_for_mutation(self, path: Path) -> bool:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
def test_mutant_generation_raises_exception_on_invalid_syntax(monkeypatch):
|
|
16
|
+
mutmut._reset_globals()
|
|
17
|
+
mutmut.config = MockConfig()
|
|
18
|
+
|
|
19
|
+
source_files = [
|
|
20
|
+
source_dir / "valid_syntax_1.py",
|
|
21
|
+
source_dir / "valid_syntax_2.py",
|
|
22
|
+
source_dir / "valid_syntax_3.py",
|
|
23
|
+
source_dir / "valid_syntax_4.py",
|
|
24
|
+
source_dir / "invalid_syntax.py",
|
|
25
|
+
]
|
|
26
|
+
monkeypatch.setattr(mutmut.__main__, "walk_source_files", lambda: source_files)
|
|
27
|
+
monkeypatch.setattr("mutmut.config.should_ignore_for_mutation", lambda _path: False)
|
|
28
|
+
|
|
29
|
+
# should raise an exception, because we copy the invalid_syntax.py file and then verify
|
|
30
|
+
# if it is valid syntax
|
|
31
|
+
with pytest.raises(InvalidGeneratedSyntaxException) as excinfo:
|
|
32
|
+
# should raise a warning, because libcst is not able to parse invalid_syntax.py
|
|
33
|
+
with pytest.warns(SyntaxWarning):
|
|
34
|
+
create_mutants(max_children=2)
|
|
35
|
+
assert 'invalid_syntax.py' in str(excinfo.value)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from unittest.mock import Mock, patch
|
|
3
3
|
import pytest
|
|
4
|
-
|
|
4
|
+
import libcst as cst
|
|
5
5
|
|
|
6
6
|
import mutmut
|
|
7
7
|
from mutmut.__main__ import (
|
|
@@ -13,8 +13,8 @@ from mutmut.__main__ import (
|
|
|
13
13
|
MutmutProgrammaticFailException,
|
|
14
14
|
CatchOutput,
|
|
15
15
|
)
|
|
16
|
-
from mutmut.trampoline_templates import trampoline_impl,
|
|
17
|
-
from mutmut.file_mutation import create_mutations, mutate_file_contents
|
|
16
|
+
from mutmut.trampoline_templates import trampoline_impl, mangle_function_name
|
|
17
|
+
from mutmut.file_mutation import create_mutations, mutate_file_contents
|
|
18
18
|
|
|
19
19
|
def mutants_for_source(source: str) -> list[str]:
|
|
20
20
|
module, mutated_nodes = create_mutations(source)
|
|
@@ -62,8 +62,47 @@ def mutated_module(source: str) -> str:
|
|
|
62
62
|
]),
|
|
63
63
|
('a.index("+")', ['a.rindex("+")', 'a.index("XX+XX")', 'a.index(None)']),
|
|
64
64
|
('a.rindex("+")', ['a.index("+")', 'a.rindex("XX+XX")', 'a.rindex(None)']),
|
|
65
|
-
('a.split()',
|
|
66
|
-
('a.rsplit()',
|
|
65
|
+
('a.split()', []),
|
|
66
|
+
('a.rsplit()', []),
|
|
67
|
+
('a.split(" ")', ['a.split("XX XX")', 'a.split(None)']),
|
|
68
|
+
('a.rsplit(" ")', ['a.rsplit("XX XX")', 'a.rsplit(None)']),
|
|
69
|
+
('a.split(sep="")', ['a.split(sep="XXXX")', 'a.split(sep=None)']),
|
|
70
|
+
('a.rsplit(sep="")', ['a.rsplit(sep="XXXX")', 'a.rsplit(sep=None)']),
|
|
71
|
+
('a.split(maxsplit=-1)', [
|
|
72
|
+
'a.rsplit(maxsplit=-1)', 'a.split(maxsplit=+1)', 'a.split(maxsplit=-2)', 'a.split(maxsplit=None)'
|
|
73
|
+
]),
|
|
74
|
+
('a.rsplit(maxsplit=-1)', [
|
|
75
|
+
'a.split(maxsplit=-1)', 'a.rsplit(maxsplit=+1)', 'a.rsplit(maxsplit=-2)', 'a.rsplit(maxsplit=None)'
|
|
76
|
+
]),
|
|
77
|
+
('a.split(" ", maxsplit=-1)', [
|
|
78
|
+
'a.split(" ", )', 'a.split(" ", maxsplit=+1)', 'a.split(" ", maxsplit=-2)',
|
|
79
|
+
'a.split(" ", maxsplit=None)', 'a.split("XX XX", maxsplit=-1)', 'a.split(None, maxsplit=-1)',
|
|
80
|
+
'a.split(maxsplit=-1)', 'a.rsplit(" ", maxsplit=-1)'
|
|
81
|
+
]),
|
|
82
|
+
('a.rsplit(" ", maxsplit=-1)', [
|
|
83
|
+
'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=+1)', 'a.rsplit(" ", maxsplit=-2)',
|
|
84
|
+
'a.rsplit(" ", maxsplit=None)', 'a.rsplit("XX XX", maxsplit=-1)', 'a.rsplit(None, maxsplit=-1)',
|
|
85
|
+
'a.rsplit(maxsplit=-1)', 'a.split(" ", maxsplit=-1)'
|
|
86
|
+
]),
|
|
87
|
+
('a.split(maxsplit=1)', ['a.split(maxsplit=2)', 'a.split(maxsplit=None)', 'a.rsplit(maxsplit=1)']),
|
|
88
|
+
('a.rsplit(maxsplit=1)', ['a.rsplit(maxsplit=2)', 'a.rsplit(maxsplit=None)', 'a.split(maxsplit=1)']),
|
|
89
|
+
('a.split(" ", 1)', [
|
|
90
|
+
'a.rsplit(" ", 1)', 'a.split(" ", )', 'a.split(" ", 2)', 'a.split(" ", None)',
|
|
91
|
+
'a.split("XX XX", 1)', 'a.split(1)', 'a.split(None, 1)'
|
|
92
|
+
]),
|
|
93
|
+
('a.rsplit(" ", 1)', [
|
|
94
|
+
'a.rsplit(" ", )', 'a.rsplit(" ", 2)', 'a.rsplit(" ", None)', 'a.rsplit("XX XX", 1)',
|
|
95
|
+
'a.rsplit(1)', 'a.rsplit(None, 1)', 'a.split(" ", 1)'
|
|
96
|
+
]),
|
|
97
|
+
('a.split(" ", maxsplit=1)', [
|
|
98
|
+
'a.rsplit(" ", maxsplit=1)', 'a.split(" ", )', 'a.split(" ", maxsplit=2)', 'a.split(" ", maxsplit=None)',
|
|
99
|
+
'a.split("XX XX", maxsplit=1)', 'a.split(None, maxsplit=1)', 'a.split(maxsplit=1)'
|
|
100
|
+
]),
|
|
101
|
+
('a.rsplit(" ", maxsplit=1)', [
|
|
102
|
+
'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=2)', 'a.rsplit(" ", maxsplit=None)',
|
|
103
|
+
'a.rsplit("XX XX", maxsplit=1)', 'a.rsplit(None, maxsplit=1)', 'a.rsplit(maxsplit=1)',
|
|
104
|
+
'a.split(" ", maxsplit=1)'
|
|
105
|
+
]),
|
|
67
106
|
('a.removeprefix("+")', ['a.removesuffix("+")', 'a.removeprefix("XX+XX")', 'a.removeprefix(None)']),
|
|
68
107
|
('a.removesuffix("+")', ['a.removeprefix("+")', 'a.removesuffix("XX+XX")', 'a.removesuffix(None)']),
|
|
69
108
|
('a.partition("++")', ['a.rpartition("++")', 'a.partition("XX++XX")', 'a.partition(None)']),
|
|
@@ -86,7 +125,9 @@ def mutated_module(source: str) -> str:
|
|
|
86
125
|
('x: list[A | None]', []),
|
|
87
126
|
('a: Optional[int] = None', 'a: Optional[int] = ""'),
|
|
88
127
|
('a: int = 1', ['a: int = 2', 'a: int = None']),
|
|
89
|
-
('a: str = "FoO"', ['a: str = "XXFoOXX"', 'a: str = "foo"', 'a: str = "FOO"', 'a: str =
|
|
128
|
+
('a: str = "FoO"', ['a: str = "XXFoOXX"', 'a: str = "foo"', 'a: str = "FOO"', 'a: str = None']),
|
|
129
|
+
(r'a: str = "Fo\t"', [r'a: str = "XXFo\tXX"', r'a: str = "FO\t"', r'a: str = "fo\t"', 'a: str = None']),
|
|
130
|
+
(r'a: str = "Fo\N{ghost} \U11223344"', [r'a: str = "XXFo\N{ghost} \U11223344XX"', r'a: str = "FO\N{GHOST} \U11223344"', r'a: str = "fo\N{ghost} \U11223344"', 'a: str = None']),
|
|
90
131
|
('lambda: 0', ['lambda: 1', 'lambda: None']),
|
|
91
132
|
("1 in (1, 2)", ['2 in (1, 2)', '1 not in (1, 2)', '1 in (2, 2)', '1 in (1, 3)']),
|
|
92
133
|
('1+1', ['2+1', '1 - 1', '1+2']),
|
|
@@ -109,9 +150,9 @@ def mutated_module(source: str) -> str:
|
|
|
109
150
|
('1e-3', '1.001'),
|
|
110
151
|
('True', 'False'),
|
|
111
152
|
('False', 'True'),
|
|
112
|
-
('"FoO"', ['"XXFoOXX"', '"foo"', '"FOO"'
|
|
113
|
-
("'FoO'", ["'XXFoOXX'", "'foo'", "'FOO'"
|
|
114
|
-
("u'FoO'", ["u'XXFoOXX'", "u'foo'", "u'FOO'"
|
|
153
|
+
('"FoO"', ['"XXFoOXX"', '"foo"', '"FOO"']),
|
|
154
|
+
("'FoO'", ["'XXFoOXX'", "'foo'", "'FOO'"]),
|
|
155
|
+
("u'FoO'", ["u'XXFoOXX'", "u'foo'", "u'FOO'"]),
|
|
115
156
|
("10", "11"),
|
|
116
157
|
("10.", "11.0"),
|
|
117
158
|
("0o10", "9"),
|
|
@@ -244,6 +285,19 @@ match x:
|
|
|
244
285
|
|
|
245
286
|
assert sorted(mutants) == sorted(expected)
|
|
246
287
|
|
|
288
|
+
def test_mach_case_does_not_mutate_bitor():
|
|
289
|
+
source = """
|
|
290
|
+
def concat():
|
|
291
|
+
match x:
|
|
292
|
+
case A() | B():
|
|
293
|
+
pass
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
mutants = mutants_for_source(source)
|
|
297
|
+
|
|
298
|
+
assert sorted(mutants) == []
|
|
299
|
+
|
|
300
|
+
|
|
247
301
|
def test_basic_class():
|
|
248
302
|
source = """
|
|
249
303
|
class Foo:
|
|
@@ -269,8 +323,8 @@ def test_function_with_annotation():
|
|
|
269
323
|
print(mutated_code)
|
|
270
324
|
|
|
271
325
|
expected_defs = [
|
|
272
|
-
'def x_capitalize__mutmut_1(s : str):\n return s[
|
|
273
|
-
'def x_capitalize__mutmut_2(s : str):\n return s[
|
|
326
|
+
'def x_capitalize__mutmut_1(s : str):\n return s[0].title() - s[1:] if s else s',
|
|
327
|
+
'def x_capitalize__mutmut_2(s : str):\n return s[1].title() + s[1:] if s else s',
|
|
274
328
|
'def x_capitalize__mutmut_3(s : str):\n return s[0].title() + s[2:] if s else s',
|
|
275
329
|
]
|
|
276
330
|
|
|
@@ -315,13 +369,10 @@ def test_mutate_dict():
|
|
|
315
369
|
assert sorted(mutants) == sorted(expected)
|
|
316
370
|
|
|
317
371
|
|
|
318
|
-
def
|
|
319
|
-
with pytest.
|
|
372
|
+
def test_syntax_error():
|
|
373
|
+
with pytest.raises(cst.ParserSyntaxError):
|
|
320
374
|
mutate_file_contents('some_file.py', ':!')
|
|
321
375
|
|
|
322
|
-
assert len(record) == 1
|
|
323
|
-
assert 'some_file.py' in record[0].message.args[0] # type: ignore
|
|
324
|
-
|
|
325
376
|
|
|
326
377
|
def test_bug_github_issue_18():
|
|
327
378
|
source = """
|
|
@@ -460,43 +511,6 @@ def foo():
|
|
|
460
511
|
assert mutated_source.count('from __future__') == 1
|
|
461
512
|
|
|
462
513
|
|
|
463
|
-
def test_preserve_generators():
|
|
464
|
-
source = '''
|
|
465
|
-
def foo():
|
|
466
|
-
yield 1
|
|
467
|
-
'''.strip()
|
|
468
|
-
mutated_source = mutated_module(source)
|
|
469
|
-
assert 'yield from _mutmut_yield_from_trampoline' in mutated_source
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
def test_is_generator():
|
|
473
|
-
source = '''
|
|
474
|
-
def foo():
|
|
475
|
-
yield 1
|
|
476
|
-
'''.strip()
|
|
477
|
-
assert is_generator(parse_statement(source)) # type: ignore
|
|
478
|
-
|
|
479
|
-
source = '''
|
|
480
|
-
def foo():
|
|
481
|
-
yield from bar()
|
|
482
|
-
'''.strip()
|
|
483
|
-
assert is_generator(parse_statement(source)) # type: ignore
|
|
484
|
-
|
|
485
|
-
source = '''
|
|
486
|
-
def foo():
|
|
487
|
-
return 1
|
|
488
|
-
'''.strip()
|
|
489
|
-
assert not is_generator(parse_statement(source)) # type: ignore
|
|
490
|
-
|
|
491
|
-
source = '''
|
|
492
|
-
def foo():
|
|
493
|
-
def bar():
|
|
494
|
-
yield 2
|
|
495
|
-
return 1
|
|
496
|
-
'''.strip()
|
|
497
|
-
assert not is_generator(parse_statement(source)) # type: ignore
|
|
498
|
-
|
|
499
|
-
|
|
500
514
|
# Negate the effects of CatchOutput because it does not play nicely with capfd in GitHub Actions
|
|
501
515
|
@patch.object(CatchOutput, 'dump_output')
|
|
502
516
|
@patch.object(CatchOutput, 'stop')
|
|
@@ -624,7 +638,6 @@ import lib
|
|
|
624
638
|
|
|
625
639
|
lib.foo()
|
|
626
640
|
{trampoline_impl.strip()}
|
|
627
|
-
{yield_from_trampoline_impl.strip()}
|
|
628
641
|
|
|
629
642
|
def x_foo__mutmut_orig(a, b):
|
|
630
643
|
return a > b
|
|
@@ -654,7 +667,7 @@ x_bar__mutmut_mutants : ClassVar[MutantDict] = {{
|
|
|
654
667
|
}}
|
|
655
668
|
|
|
656
669
|
def bar(*args, **kwargs):
|
|
657
|
-
result =
|
|
670
|
+
result = _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs)
|
|
658
671
|
return result
|
|
659
672
|
|
|
660
673
|
bar.__signature__ = _mutmut_signature(x_bar__mutmut_orig)
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
from mutmut.trampoline_templates import
|
|
2
|
-
trampoline_impl,
|
|
3
|
-
yield_from_trampoline_impl,
|
|
4
|
-
)
|
|
1
|
+
from mutmut.trampoline_templates import trampoline_impl
|
|
5
2
|
from mutmut.file_mutation import mutate_file_contents
|
|
6
3
|
|
|
7
4
|
def mutated_module(source: str) -> str:
|
|
@@ -16,7 +13,7 @@ a + 1
|
|
|
16
13
|
def foo(a, b, c):
|
|
17
14
|
return a + b * c
|
|
18
15
|
"""
|
|
19
|
-
trampolines = trampoline_impl.removesuffix('\n\n')
|
|
16
|
+
trampolines = trampoline_impl.removesuffix('\n\n')
|
|
20
17
|
|
|
21
18
|
expected = f"""
|
|
22
19
|
a + 1{trampolines}
|
|
@@ -54,7 +51,7 @@ def foo(a: List[int]) -> int:
|
|
|
54
51
|
return 1
|
|
55
52
|
"""
|
|
56
53
|
|
|
57
|
-
expected = trampoline_impl.removesuffix('\n\n') +
|
|
54
|
+
expected = trampoline_impl.removesuffix('\n\n') + """
|
|
58
55
|
def x_foo__mutmut_orig(a: List[int]) -> int:
|
|
59
56
|
return 1
|
|
60
57
|
def x_foo__mutmut_1(a: List[int]) -> int:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|