mutmut 3.2.0__tar.gz → 3.2.2__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.2.0 → mutmut-3.2.2}/HISTORY.rst +18 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/PKG-INFO +2 -1
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/.DS_Store +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__init__.py +3 -3
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__main__.py +173 -78
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
- mutmut-3.2.2/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
- mutmut-3.2.2/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/PKG-INFO +2 -1
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/requires.txt +3 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/requirements.txt +1 -0
- mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
- mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/AUTHORS.rst +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/LICENSE +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/MANIFEST.in +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/README.rst +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/result_browser_layout.tcss +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/SOURCES.txt +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/dependency_links.txt +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/entry_points.txt +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/not-zip-safe +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/top_level.txt +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/setup.cfg +0 -0
- {mutmut-3.2.0 → mutmut-3.2.2}/setup.py +0 -0
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
|
+
3.2.2
|
|
5
|
+
~~~~~
|
|
6
|
+
|
|
7
|
+
* Fixed crash when running `mutmut results`
|
|
8
|
+
|
|
9
|
+
3.2.1
|
|
10
|
+
~~~~~
|
|
11
|
+
|
|
12
|
+
* Read `paths_to_mutate` from config file
|
|
13
|
+
|
|
14
|
+
* Mutate `break` to `return` to avoid timeouts
|
|
15
|
+
|
|
16
|
+
* Added debug mode. Enable with `debug=True` in `setup.cfg` under `[mutmut]`
|
|
17
|
+
|
|
18
|
+
* Fixed new test detection. The old code incorrectly detected new tests when there were none, creating a much slower interaction loop for fixing mutants.
|
|
19
|
+
|
|
20
|
+
* And many more fixes
|
|
21
|
+
|
|
4
22
|
3.2.0
|
|
5
23
|
~~~~~
|
|
6
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.2
|
|
4
4
|
Summary: mutation testing for Python 3
|
|
5
5
|
Home-page: https://github.com/boxed/mutmut
|
|
6
6
|
Author: Anders Hovmöller
|
|
@@ -20,6 +20,7 @@ Requires-Dist: click
|
|
|
20
20
|
Requires-Dist: junit-xml==1.8
|
|
21
21
|
Requires-Dist: setproctitle
|
|
22
22
|
Requires-Dist: textual
|
|
23
|
+
Requires-Dist: toml>=0.10.2; python_version < "3.11"
|
|
23
24
|
|
|
24
25
|
mutmut - python mutation tester
|
|
25
26
|
===============================
|
|
Binary file
|
|
@@ -6,7 +6,7 @@ from parso.python.tree import (
|
|
|
6
6
|
Number,
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
-
__version__ = '3.2.
|
|
9
|
+
__version__ = '3.2.2'
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# We have a global whitelist for constants of the pattern __all__, __version__, etc
|
|
@@ -165,8 +165,8 @@ def keyword_mutation(value, context, **_):
|
|
|
165
165
|
'not': '',
|
|
166
166
|
'is': 'is not', # this will cause "is not not" sometimes, so there's a hack to fix that later
|
|
167
167
|
'in': 'not in',
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
'break': 'return',
|
|
169
|
+
'continue': 'break',
|
|
170
170
|
'True': 'False',
|
|
171
171
|
'False': 'True',
|
|
172
172
|
}.get(value)
|
|
@@ -5,7 +5,9 @@ import inspect
|
|
|
5
5
|
import itertools
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import resource
|
|
8
9
|
import shutil
|
|
10
|
+
import signal
|
|
9
11
|
import sys
|
|
10
12
|
from abc import ABC
|
|
11
13
|
from collections import defaultdict
|
|
@@ -30,17 +32,21 @@ from os import (
|
|
|
30
32
|
makedirs,
|
|
31
33
|
walk,
|
|
32
34
|
)
|
|
33
|
-
from os.path import
|
|
35
|
+
from os.path import (
|
|
36
|
+
isdir,
|
|
37
|
+
isfile,
|
|
38
|
+
)
|
|
34
39
|
from pathlib import Path
|
|
35
40
|
from signal import SIGTERM
|
|
36
41
|
from textwrap import (
|
|
37
42
|
dedent,
|
|
38
43
|
indent,
|
|
39
44
|
)
|
|
40
|
-
import resource
|
|
41
|
-
|
|
42
45
|
from threading import Thread
|
|
43
|
-
from time import
|
|
46
|
+
from time import (
|
|
47
|
+
process_time,
|
|
48
|
+
sleep,
|
|
49
|
+
)
|
|
44
50
|
from typing import (
|
|
45
51
|
Dict,
|
|
46
52
|
List,
|
|
@@ -51,23 +57,15 @@ from parso import (
|
|
|
51
57
|
parse,
|
|
52
58
|
ParserSyntaxError,
|
|
53
59
|
)
|
|
60
|
+
from rich.text import Text
|
|
54
61
|
from setproctitle import setproctitle
|
|
55
62
|
|
|
56
63
|
import mutmut
|
|
57
64
|
|
|
58
|
-
|
|
59
65
|
# Document: surviving mutants are retested when you ask mutmut to retest them, interactively in the UI or via command line
|
|
60
66
|
|
|
61
|
-
# TODO: only count a test for a function if the stack depth to get to the test < some configurable limit.
|
|
62
|
-
|
|
63
|
-
# TODO: collect tests always: first run we collect to update the known list of tests, then we run pytest with that list for stats
|
|
64
|
-
# - when we run again, we ask for all tests, check which are new and which are gone and update by running stats collection for just these
|
|
65
67
|
# TODO: pragma no mutate should end up in `skipped` category
|
|
66
68
|
# TODO: hash of function. If hash changes, retest all mutants as mutant IDs are not stable
|
|
67
|
-
# TODO: exclude mutating static typing
|
|
68
|
-
# TODO: implement timeout
|
|
69
|
-
|
|
70
|
-
# TODO: don't remove arguments to `isinstance`, as that will always fail. Same with `len`
|
|
71
69
|
|
|
72
70
|
|
|
73
71
|
NEVER_MUTATE_FUNCTION_NAMES = {'__getattribute__', '__setattr__'}
|
|
@@ -78,6 +76,7 @@ CLASS_NAME_SEPARATOR = 'ǁ'
|
|
|
78
76
|
status_by_exit_code = {
|
|
79
77
|
1: 'killed',
|
|
80
78
|
3: 'killed', # internal error in pytest means a kill
|
|
79
|
+
-24: 'killed',
|
|
81
80
|
0: 'survived',
|
|
82
81
|
5: 'no tests',
|
|
83
82
|
2: 'check was interrupted by user',
|
|
@@ -86,7 +85,9 @@ status_by_exit_code = {
|
|
|
86
85
|
34: 'skipped',
|
|
87
86
|
35: 'suspicious',
|
|
88
87
|
36: 'timeout',
|
|
88
|
+
24: 'timeout', # SIGXCPU
|
|
89
89
|
152: 'timeout', # SIGXCPU
|
|
90
|
+
255: 'timeout',
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
emoji_by_status = {
|
|
@@ -113,23 +114,24 @@ def guess_paths_to_mutate():
|
|
|
113
114
|
"""
|
|
114
115
|
this_dir = os.getcwd().split(os.sep)[-1]
|
|
115
116
|
if isdir('lib'):
|
|
116
|
-
return 'lib'
|
|
117
|
+
return ['lib']
|
|
117
118
|
elif isdir('src'):
|
|
118
|
-
return 'src'
|
|
119
|
+
return ['src']
|
|
119
120
|
elif isdir(this_dir):
|
|
120
|
-
return this_dir
|
|
121
|
+
return [this_dir]
|
|
121
122
|
elif isdir(this_dir.replace('-', '_')):
|
|
122
|
-
return this_dir.replace('-', '_')
|
|
123
|
+
return [this_dir.replace('-', '_')]
|
|
123
124
|
elif isdir(this_dir.replace(' ', '_')):
|
|
124
|
-
return this_dir.replace(' ', '_')
|
|
125
|
+
return [this_dir.replace(' ', '_')]
|
|
125
126
|
elif isdir(this_dir.replace('-', '')):
|
|
126
|
-
return this_dir.replace('-', '')
|
|
127
|
+
return [this_dir.replace('-', '')]
|
|
127
128
|
elif isdir(this_dir.replace(' ', '')):
|
|
128
|
-
return this_dir.replace(' ', '')
|
|
129
|
+
return [this_dir.replace(' ', '')]
|
|
130
|
+
if isfile(this_dir + '.py'):
|
|
131
|
+
return [this_dir + '.py']
|
|
129
132
|
raise FileNotFoundError(
|
|
130
133
|
'Could not figure out where the code to mutate is. '
|
|
131
|
-
'Please specify it
|
|
132
|
-
'or by adding "paths_to_mutate=code_dir" in setup.cfg to the [mutmut] section.')
|
|
134
|
+
'Please specify it by adding "paths_to_mutate=code_dir" in setup.cfg to the [mutmut] section.')
|
|
133
135
|
|
|
134
136
|
|
|
135
137
|
def record_trampoline_hit(name):
|
|
@@ -149,8 +151,7 @@ def record_trampoline_hit(name):
|
|
|
149
151
|
|
|
150
152
|
|
|
151
153
|
def walk_all_files():
|
|
152
|
-
|
|
153
|
-
for path in paths:
|
|
154
|
+
for path in mutmut.config.paths_to_mutate:
|
|
154
155
|
for root, dirs, files in walk(path):
|
|
155
156
|
for filename in files:
|
|
156
157
|
yield root, filename
|
|
@@ -177,6 +178,8 @@ class CollectTestsFailedException(Exception):
|
|
|
177
178
|
class BadTestExecutionCommandsException(Exception):
|
|
178
179
|
pass
|
|
179
180
|
|
|
181
|
+
|
|
182
|
+
# noinspection PyUnresolvedReferences
|
|
180
183
|
# language=python
|
|
181
184
|
trampoline_impl = """
|
|
182
185
|
from inspect import signature as _mutmut_signature
|
|
@@ -206,6 +209,7 @@ yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yiel
|
|
|
206
209
|
|
|
207
210
|
def create_mutants():
|
|
208
211
|
for path in walk_source_files():
|
|
212
|
+
print(path)
|
|
209
213
|
output_path = Path('mutants') / path
|
|
210
214
|
makedirs(output_path.parent, exist_ok=True)
|
|
211
215
|
|
|
@@ -218,6 +222,7 @@ def create_mutants():
|
|
|
218
222
|
def copy_also_copy_files():
|
|
219
223
|
assert isinstance(mutmut.config.also_copy, list)
|
|
220
224
|
for path in mutmut.config.also_copy:
|
|
225
|
+
print(' also copying', path)
|
|
221
226
|
path = Path(path)
|
|
222
227
|
destination = Path('mutants') / path
|
|
223
228
|
if not path.exists():
|
|
@@ -577,6 +582,7 @@ def yield_mutants_for_module(node, no_mutate_lines):
|
|
|
577
582
|
|
|
578
583
|
class SourceFileMutationData:
|
|
579
584
|
def __init__(self, *, path):
|
|
585
|
+
self.estimated_time_of_tests_by_mutant = {}
|
|
580
586
|
self.path = path
|
|
581
587
|
self.meta_path = Path('mutants') / (str(path) + '.meta')
|
|
582
588
|
self.meta = None
|
|
@@ -584,6 +590,7 @@ class SourceFileMutationData:
|
|
|
584
590
|
self.exit_code_by_key = {}
|
|
585
591
|
self.hash_by_function_name = {}
|
|
586
592
|
self.start_time_by_pid = {}
|
|
593
|
+
self.estimated_time_of_tests_by_pid = {}
|
|
587
594
|
|
|
588
595
|
def load(self):
|
|
589
596
|
try:
|
|
@@ -596,13 +603,14 @@ class SourceFileMutationData:
|
|
|
596
603
|
self.hash_by_function_name = self.meta.pop('hash_by_function_name')
|
|
597
604
|
assert not self.meta, self.meta # We should read all the data!
|
|
598
605
|
|
|
599
|
-
def register_pid(self, *, pid, key):
|
|
606
|
+
def register_pid(self, *, pid, key, estimated_time_of_tests):
|
|
600
607
|
self.key_by_pid[pid] = key
|
|
601
608
|
self.start_time_by_pid[pid] = datetime.now()
|
|
609
|
+
self.estimated_time_of_tests_by_pid[pid] = estimated_time_of_tests
|
|
602
610
|
|
|
603
611
|
def register_result(self, *, pid, exit_code):
|
|
604
612
|
assert self.key_by_pid[pid] in self.exit_code_by_key
|
|
605
|
-
self.exit_code_by_key[self.key_by_pid[pid]] =
|
|
613
|
+
self.exit_code_by_key[self.key_by_pid[pid]] = exit_code
|
|
606
614
|
# TODO: maybe rate limit this? Saving on each result can slow down mutation testing a lot if the test run is fast.
|
|
607
615
|
del self.key_by_pid[pid]
|
|
608
616
|
del self.start_time_by_pid[pid]
|
|
@@ -659,11 +667,7 @@ def change_cwd(path):
|
|
|
659
667
|
|
|
660
668
|
|
|
661
669
|
def collected_test_names():
|
|
662
|
-
return
|
|
663
|
-
test_name
|
|
664
|
-
for _, test_names in mutmut.tests_by_mangled_function_name.items()
|
|
665
|
-
for test_name in test_names
|
|
666
|
-
}
|
|
670
|
+
return set(mutmut.duration_by_test.keys())
|
|
667
671
|
|
|
668
672
|
|
|
669
673
|
class ListAllTestsResult:
|
|
@@ -672,12 +676,12 @@ class ListAllTestsResult:
|
|
|
672
676
|
self.ids = ids
|
|
673
677
|
|
|
674
678
|
def clear_out_obsolete_test_names(self):
|
|
675
|
-
count_before = len(mutmut.tests_by_mangled_function_name)
|
|
676
|
-
mutmut.tests_by_mangled_function_name = {
|
|
679
|
+
count_before = sum(len(x) for x in mutmut.tests_by_mangled_function_name)
|
|
680
|
+
mutmut.tests_by_mangled_function_name = defaultdict(set, **{
|
|
677
681
|
k: {test_name for test_name in test_names if test_name in self.ids}
|
|
678
682
|
for k, test_names in mutmut.tests_by_mangled_function_name.items()
|
|
679
|
-
}
|
|
680
|
-
count_after = len(mutmut.tests_by_mangled_function_name)
|
|
683
|
+
})
|
|
684
|
+
count_after = sum(len(x) for x in mutmut.tests_by_mangled_function_name)
|
|
681
685
|
if count_before != count_after:
|
|
682
686
|
print(f'Removed {count_before - count_after} obsolete test names')
|
|
683
687
|
save_stats()
|
|
@@ -687,21 +691,30 @@ class ListAllTestsResult:
|
|
|
687
691
|
|
|
688
692
|
|
|
689
693
|
class PytestRunner(TestRunner):
|
|
694
|
+
# noinspection PyMethodMayBeStatic
|
|
690
695
|
def execute_pytest(self, params, **kwargs):
|
|
691
696
|
import pytest
|
|
697
|
+
params += ['--rootdir=.']
|
|
698
|
+
if mutmut.config.debug:
|
|
699
|
+
params = ['-vv'] + params
|
|
700
|
+
print('python -m pytest ', ' '.join(params))
|
|
692
701
|
exit_code = int(pytest.main(params, **kwargs))
|
|
702
|
+
if mutmut.config.debug:
|
|
703
|
+
print(' exit code', exit_code)
|
|
693
704
|
if exit_code == 4:
|
|
694
705
|
raise BadTestExecutionCommandsException(params)
|
|
695
706
|
return exit_code
|
|
696
707
|
|
|
697
708
|
def run_stats(self, *, tests):
|
|
698
709
|
class StatsCollector:
|
|
710
|
+
# noinspection PyMethodMayBeStatic
|
|
699
711
|
def pytest_runtest_teardown(self, item, nextitem):
|
|
700
712
|
unused(nextitem)
|
|
701
713
|
for function in mutmut._stats:
|
|
702
714
|
mutmut.tests_by_mangled_function_name[function].add(strip_prefix(item._nodeid, prefix='mutants/'))
|
|
703
715
|
mutmut._stats.clear()
|
|
704
716
|
|
|
717
|
+
# noinspection PyMethodMayBeStatic
|
|
705
718
|
def pytest_runtest_makereport(self, item, call):
|
|
706
719
|
mutmut.duration_by_test[item.nodeid] = call.duration
|
|
707
720
|
|
|
@@ -739,7 +752,7 @@ class HammettRunner(TestRunner):
|
|
|
739
752
|
|
|
740
753
|
def run_stats(self, *, tests):
|
|
741
754
|
import hammett
|
|
742
|
-
print('
|
|
755
|
+
print('Running hammett stats...')
|
|
743
756
|
|
|
744
757
|
def post_test_callback(_name, **_):
|
|
745
758
|
for function in mutmut._stats:
|
|
@@ -770,7 +783,6 @@ class HammettRunner(TestRunner):
|
|
|
770
783
|
|
|
771
784
|
def mangle_function_name(*, name, class_name):
|
|
772
785
|
assert CLASS_NAME_SEPARATOR not in name
|
|
773
|
-
prefix = ''
|
|
774
786
|
if class_name:
|
|
775
787
|
assert CLASS_NAME_SEPARATOR not in class_name
|
|
776
788
|
prefix = f'x{CLASS_NAME_SEPARATOR}{class_name}{CLASS_NAME_SEPARATOR}'
|
|
@@ -845,6 +857,7 @@ def collect_stat(m: SourceFileMutationData):
|
|
|
845
857
|
for k in status_by_exit_code.values()
|
|
846
858
|
}
|
|
847
859
|
for k, v in m.exit_code_by_key.items():
|
|
860
|
+
# noinspection PyTypeChecker
|
|
848
861
|
r[status_by_exit_code[v].replace(' ', '_')] += 1
|
|
849
862
|
return Stat(
|
|
850
863
|
**r,
|
|
@@ -874,7 +887,7 @@ def print_stats(source_file_mutation_data_by_path, force_output=False):
|
|
|
874
887
|
|
|
875
888
|
def run_forced_fail(runner):
|
|
876
889
|
os.environ['MUTANT_UNDER_TEST'] = 'fail'
|
|
877
|
-
with CatchOutput(
|
|
890
|
+
with CatchOutput(spinner_title='Running forced fail test') as catcher:
|
|
878
891
|
try:
|
|
879
892
|
if runner.run_forced_fail() == 0:
|
|
880
893
|
catcher.dump_output()
|
|
@@ -887,9 +900,8 @@ def run_forced_fail(runner):
|
|
|
887
900
|
|
|
888
901
|
|
|
889
902
|
class CatchOutput:
|
|
890
|
-
def __init__(self, callback=lambda s: None,
|
|
903
|
+
def __init__(self, callback=lambda s: None, spinner_title=None):
|
|
891
904
|
self.strings = []
|
|
892
|
-
self.show_spinner = show_spinner
|
|
893
905
|
self.spinner_title = spinner_title or ''
|
|
894
906
|
|
|
895
907
|
class StdOutRedirect(TextIOBase):
|
|
@@ -898,26 +910,29 @@ class CatchOutput:
|
|
|
898
910
|
|
|
899
911
|
def write(self, s):
|
|
900
912
|
callback(s)
|
|
901
|
-
if
|
|
913
|
+
if spinner_title:
|
|
902
914
|
print_status(spinner_title)
|
|
903
915
|
self.catcher.strings.append(s)
|
|
904
916
|
return len(s)
|
|
905
917
|
self.redirect = StdOutRedirect(self)
|
|
906
918
|
|
|
919
|
+
# noinspection PyMethodMayBeStatic
|
|
907
920
|
def stop(self):
|
|
908
921
|
sys.stdout = sys.__stdout__
|
|
909
922
|
sys.stderr = sys.__stderr__
|
|
910
923
|
|
|
911
924
|
def start(self):
|
|
912
|
-
if self.
|
|
925
|
+
if self.spinner_title:
|
|
913
926
|
print_status(self.spinner_title)
|
|
914
927
|
sys.stdout = self.redirect
|
|
915
928
|
sys.stderr = self.redirect
|
|
929
|
+
if mutmut.config.debug:
|
|
930
|
+
self.stop()
|
|
916
931
|
|
|
917
932
|
def dump_output(self):
|
|
918
933
|
self.stop()
|
|
919
|
-
for
|
|
920
|
-
|
|
934
|
+
for line in self.strings:
|
|
935
|
+
print(line, end='')
|
|
921
936
|
|
|
922
937
|
def __enter__(self):
|
|
923
938
|
self.start()
|
|
@@ -925,7 +940,7 @@ class CatchOutput:
|
|
|
925
940
|
|
|
926
941
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
927
942
|
self.stop()
|
|
928
|
-
if self.
|
|
943
|
+
if self.spinner_title:
|
|
929
944
|
print()
|
|
930
945
|
|
|
931
946
|
|
|
@@ -934,6 +949,8 @@ class Config:
|
|
|
934
949
|
also_copy: List[Path]
|
|
935
950
|
do_not_mutate: List[str]
|
|
936
951
|
max_stack_depth: int
|
|
952
|
+
debug: bool
|
|
953
|
+
paths_to_mutate: List[Path]
|
|
937
954
|
|
|
938
955
|
def should_ignore_for_mutation(self, path):
|
|
939
956
|
if not str(path).endswith('.py'):
|
|
@@ -944,36 +961,67 @@ class Config:
|
|
|
944
961
|
return False
|
|
945
962
|
|
|
946
963
|
|
|
947
|
-
|
|
948
|
-
|
|
964
|
+
def config_reader():
|
|
965
|
+
path = Path('pyproject.toml')
|
|
966
|
+
if path.exists():
|
|
967
|
+
if sys.version_info >= (3, 11):
|
|
968
|
+
from tomllib import loads
|
|
969
|
+
else:
|
|
970
|
+
# noinspection PyPackageRequirements
|
|
971
|
+
from toml import loads
|
|
972
|
+
data = loads(path.read_text('utf-8'))
|
|
973
|
+
|
|
974
|
+
try:
|
|
975
|
+
config = data['tool']['mutmut']
|
|
976
|
+
except KeyError:
|
|
977
|
+
pass
|
|
978
|
+
else:
|
|
979
|
+
def s(key, default):
|
|
980
|
+
try:
|
|
981
|
+
result = config[key]
|
|
982
|
+
except KeyError:
|
|
983
|
+
return default
|
|
984
|
+
return result
|
|
985
|
+
return s
|
|
986
|
+
|
|
949
987
|
config_parser = ConfigParser()
|
|
950
988
|
config_parser.read('setup.cfg')
|
|
951
989
|
|
|
952
990
|
def s(key, default):
|
|
953
991
|
try:
|
|
954
|
-
|
|
992
|
+
result = config_parser.get('mutmut', key)
|
|
955
993
|
except (NoOptionError, NoSectionError):
|
|
956
994
|
return default
|
|
995
|
+
if isinstance(default, list):
|
|
996
|
+
result = [x for x in result.split("\n") if x]
|
|
997
|
+
elif isinstance(default, bool):
|
|
998
|
+
result = result.lower() in ('1', 't', 'true')
|
|
999
|
+
elif isinstance(default, int):
|
|
1000
|
+
result = int(result)
|
|
1001
|
+
return result
|
|
1002
|
+
return s
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@lru_cache()
|
|
1006
|
+
def read_config():
|
|
1007
|
+
s = config_reader()
|
|
957
1008
|
|
|
958
1009
|
mutmut.config = Config(
|
|
959
|
-
do_not_mutate=[
|
|
960
|
-
x
|
|
961
|
-
for x in s('do_not_mutate', '').split('\n')
|
|
962
|
-
if x
|
|
963
|
-
],
|
|
1010
|
+
do_not_mutate=s('do_not_mutate', []),
|
|
964
1011
|
also_copy=[
|
|
965
1012
|
Path(y)
|
|
966
|
-
for y in [
|
|
967
|
-
x
|
|
968
|
-
for x in s('also_copy', '').split('\n')
|
|
969
|
-
if x
|
|
970
|
-
]
|
|
1013
|
+
for y in s('also_copy', [])
|
|
971
1014
|
]+[
|
|
972
1015
|
Path('tests/'),
|
|
973
1016
|
Path('test/'),
|
|
974
1017
|
Path('tests.py'),
|
|
975
1018
|
],
|
|
976
|
-
max_stack_depth=
|
|
1019
|
+
max_stack_depth=s('max_stack_depth', -1),
|
|
1020
|
+
debug=s('debug', False),
|
|
1021
|
+
paths_to_mutate=[
|
|
1022
|
+
Path(y)
|
|
1023
|
+
for y in s('paths_to_mutate', [])
|
|
1024
|
+
] or guess_paths_to_mutate()
|
|
977
1025
|
)
|
|
978
1026
|
|
|
979
1027
|
|
|
@@ -990,7 +1038,7 @@ def run_stats_collection(runner, tests=None):
|
|
|
990
1038
|
os.environ['PY_IGNORE_IMPORTMISMATCH'] = '1'
|
|
991
1039
|
start_cpu_time = process_time()
|
|
992
1040
|
|
|
993
|
-
with CatchOutput(
|
|
1041
|
+
with CatchOutput(spinner_title='Running stats') as output_catcher:
|
|
994
1042
|
collect_stats_exit_code = runner.run_stats(tests=tests)
|
|
995
1043
|
if collect_stats_exit_code != 0:
|
|
996
1044
|
output_catcher.dump_output()
|
|
@@ -1016,7 +1064,7 @@ def collect_or_load_stats(runner):
|
|
|
1016
1064
|
run_stats_collection(runner)
|
|
1017
1065
|
else:
|
|
1018
1066
|
# Run incremental stats
|
|
1019
|
-
with CatchOutput(
|
|
1067
|
+
with CatchOutput(spinner_title='Listing all tests') as output_catcher:
|
|
1020
1068
|
os.environ['MUTANT_UNDER_TEST'] = 'list_all_tests'
|
|
1021
1069
|
try:
|
|
1022
1070
|
all_tests_result = runner.list_all_tests()
|
|
@@ -1030,6 +1078,7 @@ def collect_or_load_stats(runner):
|
|
|
1030
1078
|
new_tests = all_tests_result.new_tests()
|
|
1031
1079
|
|
|
1032
1080
|
if new_tests:
|
|
1081
|
+
print(f'Found {len(new_tests)} new tests, rerunning stats collection')
|
|
1033
1082
|
run_stats_collection(runner, tests=new_tests)
|
|
1034
1083
|
|
|
1035
1084
|
|
|
@@ -1133,6 +1182,23 @@ def stop_all_children(mutants):
|
|
|
1133
1182
|
m.stop_children()
|
|
1134
1183
|
|
|
1135
1184
|
|
|
1185
|
+
def timeout_checker(mutants):
|
|
1186
|
+
def inner_timout_checker():
|
|
1187
|
+
while True:
|
|
1188
|
+
sleep(1)
|
|
1189
|
+
|
|
1190
|
+
now = datetime.now()
|
|
1191
|
+
for m, mutant_name, result in mutants:
|
|
1192
|
+
for pid, start_time in m.start_time_by_pid.items():
|
|
1193
|
+
run_time = now - start_time
|
|
1194
|
+
if run_time.total_seconds() > (m.estimated_time_of_tests_by_mutant[mutant_name] + 1) * 4:
|
|
1195
|
+
try:
|
|
1196
|
+
os.kill(pid, signal.SIGXCPU)
|
|
1197
|
+
except ProcessLookupError:
|
|
1198
|
+
pass
|
|
1199
|
+
return inner_timout_checker
|
|
1200
|
+
|
|
1201
|
+
|
|
1136
1202
|
@cli.command()
|
|
1137
1203
|
@click.option('--max-children', type=int)
|
|
1138
1204
|
@click.argument('mutant_names', required=False, nargs=-1)
|
|
@@ -1141,13 +1207,15 @@ def run(mutant_names, *, max_children):
|
|
|
1141
1207
|
|
|
1142
1208
|
# TODO: run no-ops once in a while to detect if we get false negatives
|
|
1143
1209
|
# TODO: we should be able to get information on which tests killed mutants, which means we can get a list of tests and how many mutants each test kills. Those that kill zero mutants are redundant!
|
|
1144
|
-
|
|
1145
|
-
start = datetime.now()
|
|
1146
|
-
print('generating mutants')
|
|
1147
1210
|
os.environ['MUTANT_UNDER_TEST'] = 'mutant_generation'
|
|
1148
1211
|
read_config()
|
|
1149
|
-
|
|
1150
|
-
|
|
1212
|
+
|
|
1213
|
+
start = datetime.now()
|
|
1214
|
+
makedirs(Path('mutants'), exist_ok=True)
|
|
1215
|
+
with CatchOutput(spinner_title='Generating mutants'):
|
|
1216
|
+
create_mutants()
|
|
1217
|
+
copy_also_copy_files()
|
|
1218
|
+
|
|
1151
1219
|
time = datetime.now() - start
|
|
1152
1220
|
print(f' done in {round(time.total_seconds()*1000)}ms', )
|
|
1153
1221
|
|
|
@@ -1172,13 +1240,13 @@ def run(mutant_names, *, max_children):
|
|
|
1172
1240
|
mutants, source_file_mutation_data_by_path = collect_source_file_mutation_data(mutant_names=mutant_names)
|
|
1173
1241
|
|
|
1174
1242
|
os.environ['MUTANT_UNDER_TEST'] = ''
|
|
1175
|
-
with CatchOutput(
|
|
1243
|
+
with CatchOutput(spinner_title='Running clean tests') as output_catcher:
|
|
1176
1244
|
tests = tests_for_mutant_names(mutant_names)
|
|
1177
1245
|
|
|
1178
1246
|
clean_test_exit_code = runner.run_tests(mutant_name=None, tests=tests)
|
|
1179
1247
|
if clean_test_exit_code != 0:
|
|
1180
1248
|
output_catcher.dump_output()
|
|
1181
|
-
print('
|
|
1249
|
+
print('Failed to run clean test')
|
|
1182
1250
|
exit(1)
|
|
1183
1251
|
print(' done')
|
|
1184
1252
|
|
|
@@ -1188,7 +1256,10 @@ def run(mutant_names, *, max_children):
|
|
|
1188
1256
|
runner.prepare_main_test_run()
|
|
1189
1257
|
|
|
1190
1258
|
def read_one_child_exit_status():
|
|
1191
|
-
pid,
|
|
1259
|
+
pid, wait_status = os.wait()
|
|
1260
|
+
exit_code = os.waitstatus_to_exitcode(wait_status)
|
|
1261
|
+
if mutmut.config.debug:
|
|
1262
|
+
print(' worker exit code', exit_code)
|
|
1192
1263
|
source_file_mutation_data_by_pid[pid].register_result(pid=pid, exit_code=exit_code)
|
|
1193
1264
|
|
|
1194
1265
|
source_file_mutation_data_by_pid: Dict[int, SourceFileMutationData] = {} # many pids map to one MutationData
|
|
@@ -1207,6 +1278,16 @@ def run(mutant_names, *, max_children):
|
|
|
1207
1278
|
try:
|
|
1208
1279
|
print('Running mutation testing')
|
|
1209
1280
|
|
|
1281
|
+
# Calculate times of tests
|
|
1282
|
+
for m, mutant_name, result in mutants:
|
|
1283
|
+
mutant_name = mutant_name.replace('__init__.', '')
|
|
1284
|
+
tests = mutmut.tests_by_mangled_function_name.get(mangled_name_from_mutant_name(mutant_name), [])
|
|
1285
|
+
estimated_time_of_tests = sum(mutmut.duration_by_test[test_name] for test_name in tests)
|
|
1286
|
+
m.estimated_time_of_tests_by_mutant[mutant_name] = estimated_time_of_tests
|
|
1287
|
+
|
|
1288
|
+
Thread(target=timeout_checker(mutants), daemon=True).start()
|
|
1289
|
+
|
|
1290
|
+
# Now do mutation
|
|
1210
1291
|
for m, mutant_name, result in mutants:
|
|
1211
1292
|
print_stats(source_file_mutation_data_by_path)
|
|
1212
1293
|
|
|
@@ -1235,8 +1316,9 @@ def run(mutant_names, *, max_children):
|
|
|
1235
1316
|
if not tests:
|
|
1236
1317
|
os._exit(33)
|
|
1237
1318
|
|
|
1238
|
-
estimated_time_of_tests =
|
|
1239
|
-
|
|
1319
|
+
estimated_time_of_tests = m.estimated_time_of_tests_by_mutant[mutant_name]
|
|
1320
|
+
cpu_time_limit = ceil((estimated_time_of_tests + 1) * 2 + process_time()) * 10
|
|
1321
|
+
resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit))
|
|
1240
1322
|
|
|
1241
1323
|
with CatchOutput():
|
|
1242
1324
|
result = runner.run_tests(mutant_name=mutant_name, tests=tests)
|
|
@@ -1248,7 +1330,7 @@ def run(mutant_names, *, max_children):
|
|
|
1248
1330
|
else:
|
|
1249
1331
|
# in the parent
|
|
1250
1332
|
source_file_mutation_data_by_pid[pid] = m
|
|
1251
|
-
m.register_pid(pid=pid, key=mutant_name)
|
|
1333
|
+
m.register_pid(pid=pid, key=mutant_name, estimated_time_of_tests=estimated_time_of_tests)
|
|
1252
1334
|
running_children += 1
|
|
1253
1335
|
|
|
1254
1336
|
if running_children >= max_children:
|
|
@@ -1264,7 +1346,7 @@ def run(mutant_names, *, max_children):
|
|
|
1264
1346
|
except ChildProcessError:
|
|
1265
1347
|
pass
|
|
1266
1348
|
except KeyboardInterrupt:
|
|
1267
|
-
print('
|
|
1349
|
+
print('Stopping...')
|
|
1268
1350
|
stop_all_children(mutants)
|
|
1269
1351
|
|
|
1270
1352
|
t = datetime.now() - start
|
|
@@ -1303,6 +1385,7 @@ def tests_for_mutant_names(mutant_names):
|
|
|
1303
1385
|
@cli.command()
|
|
1304
1386
|
@click.option('--all', default=False)
|
|
1305
1387
|
def results(all):
|
|
1388
|
+
read_config()
|
|
1306
1389
|
for path in walk_source_files():
|
|
1307
1390
|
if not str(path).endswith('.py'):
|
|
1308
1391
|
continue
|
|
@@ -1441,6 +1524,8 @@ def apply_mutant(mutant_name):
|
|
|
1441
1524
|
|
|
1442
1525
|
@cli.command()
|
|
1443
1526
|
def browse():
|
|
1527
|
+
read_config()
|
|
1528
|
+
|
|
1444
1529
|
from textual.app import App
|
|
1445
1530
|
from textual.containers import Container
|
|
1446
1531
|
from textual.widgets import Footer
|
|
@@ -1463,7 +1548,7 @@ def browse():
|
|
|
1463
1548
|
columns = [
|
|
1464
1549
|
('path', 'Path'),
|
|
1465
1550
|
] + [
|
|
1466
|
-
(status, emoji)
|
|
1551
|
+
(status, Text(emoji, justify='right'))
|
|
1467
1552
|
for status, emoji in emoji_by_status.items()
|
|
1468
1553
|
]
|
|
1469
1554
|
|
|
@@ -1480,12 +1565,14 @@ def browse():
|
|
|
1480
1565
|
|
|
1481
1566
|
def on_mount(self):
|
|
1482
1567
|
# files table
|
|
1568
|
+
# noinspection PyTypeChecker
|
|
1483
1569
|
files_table: DataTable = self.query_one('#files')
|
|
1484
1570
|
files_table.cursor_type = 'row'
|
|
1485
1571
|
for key, label in self.columns:
|
|
1486
1572
|
files_table.add_column(key=key, label=label)
|
|
1487
1573
|
|
|
1488
1574
|
# mutants table
|
|
1575
|
+
# noinspection PyTypeChecker
|
|
1489
1576
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1490
1577
|
mutants_table.cursor_type = 'row'
|
|
1491
1578
|
mutants_table.add_columns('name', 'status')
|
|
@@ -1504,17 +1591,21 @@ def browse():
|
|
|
1504
1591
|
source_file_mutation_data.load()
|
|
1505
1592
|
stat = collect_stat(source_file_mutation_data)
|
|
1506
1593
|
|
|
1507
|
-
self.source_file_mutation_data_and_stat_by_path[p] = source_file_mutation_data, stat
|
|
1594
|
+
self.source_file_mutation_data_and_stat_by_path[str(p)] = source_file_mutation_data, stat
|
|
1508
1595
|
|
|
1509
1596
|
def populate_files_table(self):
|
|
1597
|
+
# noinspection PyTypeChecker
|
|
1510
1598
|
files_table: DataTable = self.query_one('#files')
|
|
1511
1599
|
# TODO: restore selection
|
|
1512
1600
|
selected_row = files_table.cursor_row
|
|
1513
1601
|
files_table.clear()
|
|
1514
1602
|
|
|
1515
1603
|
for p, (source_file_mutation_data, stat) in sorted(self.source_file_mutation_data_and_stat_by_path.items()):
|
|
1516
|
-
row = [p] + [
|
|
1517
|
-
|
|
1604
|
+
row = [p] + [
|
|
1605
|
+
Text(str(getattr(stat, k.replace(' ', '_'))), justify="right")
|
|
1606
|
+
for k, _ in self.columns[1:]
|
|
1607
|
+
]
|
|
1608
|
+
files_table.add_row(*row, key=str(p))
|
|
1518
1609
|
|
|
1519
1610
|
files_table.move_cursor(row=selected_row)
|
|
1520
1611
|
|
|
@@ -1522,6 +1613,7 @@ def browse():
|
|
|
1522
1613
|
if not event.row_key or not event.row_key.value:
|
|
1523
1614
|
return
|
|
1524
1615
|
if event.data_table.id == 'files':
|
|
1616
|
+
# noinspection PyTypeChecker
|
|
1525
1617
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1526
1618
|
mutants_table.clear()
|
|
1527
1619
|
source_file_mutation_data, stat = self.source_file_mutation_data_and_stat_by_path[event.row_key.value]
|
|
@@ -1532,7 +1624,8 @@ def browse():
|
|
|
1532
1624
|
mutants_table.add_row(k, emoji_by_status[status], key=k)
|
|
1533
1625
|
else:
|
|
1534
1626
|
assert event.data_table.id == 'mutants'
|
|
1535
|
-
|
|
1627
|
+
# noinspection PyTypeChecker
|
|
1628
|
+
diff_view: Static = self.query_one('#diff_view')
|
|
1536
1629
|
if event.row_key.value is None:
|
|
1537
1630
|
diff_view.update('')
|
|
1538
1631
|
else:
|
|
@@ -1562,6 +1655,7 @@ def browse():
|
|
|
1562
1655
|
self.populate_files_table()
|
|
1563
1656
|
|
|
1564
1657
|
def get_mutant_name_from_selection(self):
|
|
1658
|
+
# noinspection PyTypeChecker
|
|
1565
1659
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1566
1660
|
if mutants_table.cursor_row is None:
|
|
1567
1661
|
return
|
|
@@ -1579,6 +1673,7 @@ def browse():
|
|
|
1579
1673
|
|
|
1580
1674
|
def action_apply_mutant(self):
|
|
1581
1675
|
read_config()
|
|
1676
|
+
# noinspection PyTypeChecker
|
|
1582
1677
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1583
1678
|
if mutants_table.cursor_row is None:
|
|
1584
1679
|
return
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.2
|
|
4
4
|
Summary: mutation testing for Python 3
|
|
5
5
|
Home-page: https://github.com/boxed/mutmut
|
|
6
6
|
Author: Anders Hovmöller
|
|
@@ -20,6 +20,7 @@ Requires-Dist: click
|
|
|
20
20
|
Requires-Dist: junit-xml==1.8
|
|
21
21
|
Requires-Dist: setproctitle
|
|
22
22
|
Requires-Dist: textual
|
|
23
|
+
Requires-Dist: toml>=0.10.2; python_version < "3.11"
|
|
23
24
|
|
|
24
25
|
mutmut - python mutation tester
|
|
25
26
|
===============================
|
|
Binary file
|
|
Binary file
|
|
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
|