mutmut 3.1.0__tar.gz → 3.2.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.1.0 → mutmut-3.2.1}/AUTHORS.rst +1 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/HISTORY.rst +27 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/PKG-INFO +2 -1
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/.DS_Store +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/__init__.py +12 -3
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/__main__.py +214 -101
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
- mutmut-3.2.1/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
- mutmut-3.2.1/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut/result_browser_layout.tcss +8 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/PKG-INFO +2 -1
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/requires.txt +3 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/requirements.txt +1 -0
- mutmut-3.1.0/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
- mutmut-3.1.0/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/LICENSE +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/MANIFEST.in +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/README.rst +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/SOURCES.txt +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/dependency_links.txt +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/entry_points.txt +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/not-zip-safe +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/mutmut.egg-info/top_level.txt +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/setup.cfg +0 -0
- {mutmut-3.1.0 → mutmut-3.2.1}/setup.py +0 -0
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
|
+
3.2.1
|
|
5
|
+
~~~~~
|
|
6
|
+
|
|
7
|
+
* Read `paths_to_mutate` from config file
|
|
8
|
+
|
|
9
|
+
* Mutate `break` to `return` to avoid timeouts
|
|
10
|
+
|
|
11
|
+
* Added debug mode. Enable with `debug=True` in `setup.cfg` under `[mutmut]`
|
|
12
|
+
|
|
13
|
+
* Fixed new test detection. The old code incorrectly detected new tests when there were none, creating a much slower interaction loop for fixing mutants.
|
|
14
|
+
|
|
15
|
+
* And many more fixes
|
|
16
|
+
|
|
17
|
+
3.2.0
|
|
18
|
+
~~~~~
|
|
19
|
+
|
|
20
|
+
* Timeouts for mutants implemented.
|
|
21
|
+
|
|
22
|
+
* Browser: syntax highlighting for diff view
|
|
23
|
+
|
|
24
|
+
* More fixes for generators.
|
|
25
|
+
|
|
26
|
+
* Fix for `src`-style layout of projects.
|
|
27
|
+
|
|
28
|
+
* Fixed bug where mutmut would recollect all tests on every run, slowing down startup.
|
|
29
|
+
|
|
30
|
+
|
|
4
31
|
3.1.0
|
|
5
32
|
~~~~~
|
|
6
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
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
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
1
3
|
from parso.python.tree import (
|
|
2
4
|
Keyword,
|
|
3
5
|
Name,
|
|
4
6
|
Number,
|
|
5
7
|
)
|
|
6
8
|
|
|
7
|
-
__version__ = '3.1
|
|
9
|
+
__version__ = '3.2.1'
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
# We have a global whitelist for constants of the pattern __all__, __version__, etc
|
|
@@ -22,6 +24,13 @@ dunder_whitelist = [
|
|
|
22
24
|
'copyright',
|
|
23
25
|
]
|
|
24
26
|
|
|
27
|
+
duration_by_test = {}
|
|
28
|
+
stats_time = None
|
|
29
|
+
config = None
|
|
30
|
+
|
|
31
|
+
_stats = set()
|
|
32
|
+
tests_by_mangled_function_name = defaultdict(set)
|
|
33
|
+
|
|
25
34
|
|
|
26
35
|
class SkipException(Exception):
|
|
27
36
|
pass
|
|
@@ -156,8 +165,8 @@ def keyword_mutation(value, context, **_):
|
|
|
156
165
|
'not': '',
|
|
157
166
|
'is': 'is not', # this will cause "is not not" sometimes, so there's a hack to fix that later
|
|
158
167
|
'in': 'not in',
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
'break': 'return',
|
|
169
|
+
'continue': 'break',
|
|
161
170
|
'True': 'False',
|
|
162
171
|
'False': 'True',
|
|
163
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
|
|
@@ -25,11 +27,15 @@ from functools import lru_cache
|
|
|
25
27
|
from hashlib import md5
|
|
26
28
|
from io import TextIOBase
|
|
27
29
|
from json import JSONDecodeError
|
|
30
|
+
from math import ceil
|
|
28
31
|
from os import (
|
|
29
32
|
makedirs,
|
|
30
33
|
walk,
|
|
31
34
|
)
|
|
32
|
-
from os.path import
|
|
35
|
+
from os.path import (
|
|
36
|
+
isdir,
|
|
37
|
+
isfile,
|
|
38
|
+
)
|
|
33
39
|
from pathlib import Path
|
|
34
40
|
from signal import SIGTERM
|
|
35
41
|
from textwrap import (
|
|
@@ -37,6 +43,10 @@ from textwrap import (
|
|
|
37
43
|
indent,
|
|
38
44
|
)
|
|
39
45
|
from threading import Thread
|
|
46
|
+
from time import (
|
|
47
|
+
process_time,
|
|
48
|
+
sleep,
|
|
49
|
+
)
|
|
40
50
|
from typing import (
|
|
41
51
|
Dict,
|
|
42
52
|
List,
|
|
@@ -47,37 +57,26 @@ from parso import (
|
|
|
47
57
|
parse,
|
|
48
58
|
ParserSyntaxError,
|
|
49
59
|
)
|
|
60
|
+
from rich.text import Text
|
|
50
61
|
from setproctitle import setproctitle
|
|
51
62
|
|
|
52
63
|
import mutmut
|
|
53
64
|
|
|
54
|
-
mutmut.duration_by_test = {}
|
|
55
|
-
|
|
56
65
|
# Document: surviving mutants are retested when you ask mutmut to retest them, interactively in the UI or via command line
|
|
57
66
|
|
|
58
|
-
# TODO: only count a test for a function if the stack depth to get to the test < some configurable limit.
|
|
59
|
-
|
|
60
|
-
# TODO: collect tests always: first run we collect to update the known list of tests, then we run pytest with that list for stats
|
|
61
|
-
# - 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
|
|
62
67
|
# TODO: pragma no mutate should end up in `skipped` category
|
|
63
68
|
# TODO: hash of function. If hash changes, retest all mutants as mutant IDs are not stable
|
|
64
|
-
# TODO: exclude mutating static typing
|
|
65
|
-
# TODO: implement timeout
|
|
66
|
-
|
|
67
|
-
# TODO: don't remove arguments to `isinstance`, as that will always fail. Same with `len`
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
NEVER_MUTATE_FUNCTION_NAMES = {'__getattribute__', '__setattr__'}
|
|
71
72
|
NEVER_MUTATE_FUNCTION_CALLS = {'isinstance', 'len'}
|
|
72
73
|
CLASS_NAME_SEPARATOR = 'ǁ'
|
|
73
74
|
|
|
74
|
-
mutmut._stats = set()
|
|
75
|
-
mutmut.tests_by_mangled_function_name = defaultdict(set)
|
|
76
|
-
|
|
77
75
|
|
|
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,6 +85,9 @@ status_by_exit_code = {
|
|
|
86
85
|
34: 'skipped',
|
|
87
86
|
35: 'suspicious',
|
|
88
87
|
36: 'timeout',
|
|
88
|
+
24: 'timeout', # SIGXCPU
|
|
89
|
+
152: 'timeout', # SIGXCPU
|
|
90
|
+
255: 'timeout',
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
emoji_by_status = {
|
|
@@ -112,23 +114,24 @@ def guess_paths_to_mutate():
|
|
|
112
114
|
"""
|
|
113
115
|
this_dir = os.getcwd().split(os.sep)[-1]
|
|
114
116
|
if isdir('lib'):
|
|
115
|
-
return 'lib'
|
|
117
|
+
return ['lib']
|
|
116
118
|
elif isdir('src'):
|
|
117
|
-
return 'src'
|
|
119
|
+
return ['src']
|
|
118
120
|
elif isdir(this_dir):
|
|
119
|
-
return this_dir
|
|
121
|
+
return [this_dir]
|
|
120
122
|
elif isdir(this_dir.replace('-', '_')):
|
|
121
|
-
return this_dir.replace('-', '_')
|
|
123
|
+
return [this_dir.replace('-', '_')]
|
|
122
124
|
elif isdir(this_dir.replace(' ', '_')):
|
|
123
|
-
return this_dir.replace(' ', '_')
|
|
125
|
+
return [this_dir.replace(' ', '_')]
|
|
124
126
|
elif isdir(this_dir.replace('-', '')):
|
|
125
|
-
return this_dir.replace('-', '')
|
|
127
|
+
return [this_dir.replace('-', '')]
|
|
126
128
|
elif isdir(this_dir.replace(' ', '')):
|
|
127
|
-
return this_dir.replace(' ', '')
|
|
129
|
+
return [this_dir.replace(' ', '')]
|
|
130
|
+
if isfile(this_dir + '.py'):
|
|
131
|
+
return [this_dir + '.py']
|
|
128
132
|
raise FileNotFoundError(
|
|
129
133
|
'Could not figure out where the code to mutate is. '
|
|
130
|
-
'Please specify it
|
|
131
|
-
'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.')
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
def record_trampoline_hit(name):
|
|
@@ -148,8 +151,7 @@ def record_trampoline_hit(name):
|
|
|
148
151
|
|
|
149
152
|
|
|
150
153
|
def walk_all_files():
|
|
151
|
-
|
|
152
|
-
for path in paths:
|
|
154
|
+
for path in mutmut.config.paths_to_mutate:
|
|
153
155
|
for root, dirs, files in walk(path):
|
|
154
156
|
for filename in files:
|
|
155
157
|
yield root, filename
|
|
@@ -176,6 +178,8 @@ class CollectTestsFailedException(Exception):
|
|
|
176
178
|
class BadTestExecutionCommandsException(Exception):
|
|
177
179
|
pass
|
|
178
180
|
|
|
181
|
+
|
|
182
|
+
# noinspection PyUnresolvedReferences
|
|
179
183
|
# language=python
|
|
180
184
|
trampoline_impl = """
|
|
181
185
|
from inspect import signature as _mutmut_signature
|
|
@@ -189,22 +193,23 @@ def _mutmut_trampoline(orig, mutants, *args, **kwargs):
|
|
|
189
193
|
elif mutant_under_test == 'stats':
|
|
190
194
|
from mutmut.__main__ import record_trampoline_hit
|
|
191
195
|
record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
|
|
192
|
-
|
|
193
|
-
return # for the yield case
|
|
196
|
+
result = orig(*args, **kwargs)
|
|
197
|
+
return result # for the yield case
|
|
194
198
|
prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
|
|
195
199
|
if not mutant_under_test.startswith(prefix):
|
|
196
|
-
|
|
197
|
-
return # for the yield case
|
|
200
|
+
result = orig(*args, **kwargs)
|
|
201
|
+
return result # for the yield case
|
|
198
202
|
mutant_name = mutant_under_test.rpartition('.')[-1]
|
|
199
|
-
|
|
203
|
+
result = mutants[mutant_name](*args, **kwargs)
|
|
204
|
+
return result
|
|
200
205
|
|
|
201
206
|
"""
|
|
202
|
-
yield_from_trampoline_impl = trampoline_impl.replace('
|
|
203
|
-
trampoline_impl = trampoline_impl.replace('ret ', 'return ')
|
|
207
|
+
yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
|
|
204
208
|
|
|
205
209
|
|
|
206
210
|
def create_mutants():
|
|
207
211
|
for path in walk_source_files():
|
|
212
|
+
print(path)
|
|
208
213
|
output_path = Path('mutants') / path
|
|
209
214
|
makedirs(output_path.parent, exist_ok=True)
|
|
210
215
|
|
|
@@ -217,6 +222,7 @@ def create_mutants():
|
|
|
217
222
|
def copy_also_copy_files():
|
|
218
223
|
assert isinstance(mutmut.config.also_copy, list)
|
|
219
224
|
for path in mutmut.config.also_copy:
|
|
225
|
+
print(' also copying', path)
|
|
220
226
|
path = Path(path)
|
|
221
227
|
destination = Path('mutants') / path
|
|
222
228
|
if not path.exists():
|
|
@@ -257,7 +263,8 @@ def create_mutants_for_file(filename, output_path):
|
|
|
257
263
|
exit(1)
|
|
258
264
|
|
|
259
265
|
source_file_mutation_data = SourceFileMutationData(path=filename)
|
|
260
|
-
module_name = str(filename)[:-len(filename.suffix)].replace(os.sep, '.')
|
|
266
|
+
module_name = strip_prefix(str(filename)[:-len(filename.suffix)].replace(os.sep, '.'), prefix='src.')
|
|
267
|
+
|
|
261
268
|
source_file_mutation_data.exit_code_by_key = {
|
|
262
269
|
'.'.join([module_name, x]).replace('.__init__.', '.'): None
|
|
263
270
|
for x in mutant_names
|
|
@@ -314,17 +321,18 @@ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
|
|
|
314
321
|
access_suffix = '")'
|
|
315
322
|
|
|
316
323
|
if is_generator:
|
|
317
|
-
|
|
324
|
+
yield_statement = 'yield from ' # note the space at the end!
|
|
318
325
|
trampoline_name = '_mutmut_yield_from_trampoline'
|
|
319
326
|
else:
|
|
320
|
-
|
|
327
|
+
yield_statement = ''
|
|
321
328
|
trampoline_name = '_mutmut_trampoline'
|
|
322
329
|
|
|
323
330
|
return f"""
|
|
324
331
|
{mutants_dict}
|
|
325
332
|
|
|
326
333
|
def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
|
|
327
|
-
{
|
|
334
|
+
result = {yield_statement}{trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, *args, **kwargs)
|
|
335
|
+
return result
|
|
328
336
|
|
|
329
337
|
{orig_name}.__signature__ = _mutmut_signature({mangled_name}__mutmut_orig)
|
|
330
338
|
{mangled_name}__mutmut_orig.__name__ = '{mangled_name}'
|
|
@@ -574,6 +582,7 @@ def yield_mutants_for_module(node, no_mutate_lines):
|
|
|
574
582
|
|
|
575
583
|
class SourceFileMutationData:
|
|
576
584
|
def __init__(self, *, path):
|
|
585
|
+
self.estimated_time_of_tests_by_mutant = {}
|
|
577
586
|
self.path = path
|
|
578
587
|
self.meta_path = Path('mutants') / (str(path) + '.meta')
|
|
579
588
|
self.meta = None
|
|
@@ -581,6 +590,7 @@ class SourceFileMutationData:
|
|
|
581
590
|
self.exit_code_by_key = {}
|
|
582
591
|
self.hash_by_function_name = {}
|
|
583
592
|
self.start_time_by_pid = {}
|
|
593
|
+
self.estimated_time_of_tests_by_pid = {}
|
|
584
594
|
|
|
585
595
|
def load(self):
|
|
586
596
|
try:
|
|
@@ -593,13 +603,14 @@ class SourceFileMutationData:
|
|
|
593
603
|
self.hash_by_function_name = self.meta.pop('hash_by_function_name')
|
|
594
604
|
assert not self.meta, self.meta # We should read all the data!
|
|
595
605
|
|
|
596
|
-
def register_pid(self, *, pid, key):
|
|
606
|
+
def register_pid(self, *, pid, key, estimated_time_of_tests):
|
|
597
607
|
self.key_by_pid[pid] = key
|
|
598
608
|
self.start_time_by_pid[pid] = datetime.now()
|
|
609
|
+
self.estimated_time_of_tests_by_pid[pid] = estimated_time_of_tests
|
|
599
610
|
|
|
600
611
|
def register_result(self, *, pid, exit_code):
|
|
601
612
|
assert self.key_by_pid[pid] in self.exit_code_by_key
|
|
602
|
-
self.exit_code_by_key[self.key_by_pid[pid]] =
|
|
613
|
+
self.exit_code_by_key[self.key_by_pid[pid]] = exit_code
|
|
603
614
|
# TODO: maybe rate limit this? Saving on each result can slow down mutation testing a lot if the test run is fast.
|
|
604
615
|
del self.key_by_pid[pid]
|
|
605
616
|
del self.start_time_by_pid[pid]
|
|
@@ -655,46 +666,55 @@ def change_cwd(path):
|
|
|
655
666
|
os.chdir(old_cwd)
|
|
656
667
|
|
|
657
668
|
|
|
669
|
+
def collected_test_names():
|
|
670
|
+
return set(mutmut.duration_by_test.keys())
|
|
671
|
+
|
|
672
|
+
|
|
658
673
|
class ListAllTestsResult:
|
|
659
674
|
def __init__(self, *, ids):
|
|
660
675
|
assert isinstance(ids, set)
|
|
661
676
|
self.ids = ids
|
|
662
677
|
|
|
663
|
-
@staticmethod
|
|
664
|
-
def old_tests():
|
|
665
|
-
return {
|
|
666
|
-
test_name
|
|
667
|
-
for _, test_names in mutmut.tests_by_mangled_function_name.items()
|
|
668
|
-
for test_name in test_names
|
|
669
|
-
}
|
|
670
|
-
|
|
671
678
|
def clear_out_obsolete_test_names(self):
|
|
672
|
-
mutmut.tests_by_mangled_function_name
|
|
673
|
-
|
|
679
|
+
count_before = sum(len(x) for x in mutmut.tests_by_mangled_function_name)
|
|
680
|
+
mutmut.tests_by_mangled_function_name = defaultdict(set, **{
|
|
681
|
+
k: {test_name for test_name in test_names if test_name in self.ids}
|
|
674
682
|
for k, test_names in mutmut.tests_by_mangled_function_name.items()
|
|
675
|
-
}
|
|
676
|
-
|
|
683
|
+
})
|
|
684
|
+
count_after = sum(len(x) for x in mutmut.tests_by_mangled_function_name)
|
|
685
|
+
if count_before != count_after:
|
|
686
|
+
print(f'Removed {count_before - count_after} obsolete test names')
|
|
687
|
+
save_stats()
|
|
677
688
|
|
|
678
689
|
def new_tests(self):
|
|
679
|
-
return self.ids -
|
|
690
|
+
return self.ids - collected_test_names()
|
|
680
691
|
|
|
681
692
|
|
|
682
693
|
class PytestRunner(TestRunner):
|
|
694
|
+
# noinspection PyMethodMayBeStatic
|
|
683
695
|
def execute_pytest(self, params, **kwargs):
|
|
684
696
|
import pytest
|
|
697
|
+
params += ['--rootdir=.']
|
|
698
|
+
if mutmut.config.debug:
|
|
699
|
+
params = ['-vv'] + params
|
|
700
|
+
print('python -m pytest ', ' '.join(params))
|
|
685
701
|
exit_code = int(pytest.main(params, **kwargs))
|
|
702
|
+
if mutmut.config.debug:
|
|
703
|
+
print(' exit code', exit_code)
|
|
686
704
|
if exit_code == 4:
|
|
687
705
|
raise BadTestExecutionCommandsException(params)
|
|
688
706
|
return exit_code
|
|
689
707
|
|
|
690
708
|
def run_stats(self, *, tests):
|
|
691
709
|
class StatsCollector:
|
|
710
|
+
# noinspection PyMethodMayBeStatic
|
|
692
711
|
def pytest_runtest_teardown(self, item, nextitem):
|
|
693
712
|
unused(nextitem)
|
|
694
713
|
for function in mutmut._stats:
|
|
695
714
|
mutmut.tests_by_mangled_function_name[function].add(strip_prefix(item._nodeid, prefix='mutants/'))
|
|
696
715
|
mutmut._stats.clear()
|
|
697
716
|
|
|
717
|
+
# noinspection PyMethodMayBeStatic
|
|
698
718
|
def pytest_runtest_makereport(self, item, call):
|
|
699
719
|
mutmut.duration_by_test[item.nodeid] = call.duration
|
|
700
720
|
|
|
@@ -732,7 +752,7 @@ class HammettRunner(TestRunner):
|
|
|
732
752
|
|
|
733
753
|
def run_stats(self, *, tests):
|
|
734
754
|
import hammett
|
|
735
|
-
print('
|
|
755
|
+
print('Running hammett stats...')
|
|
736
756
|
|
|
737
757
|
def post_test_callback(_name, **_):
|
|
738
758
|
for function in mutmut._stats:
|
|
@@ -763,7 +783,6 @@ class HammettRunner(TestRunner):
|
|
|
763
783
|
|
|
764
784
|
def mangle_function_name(*, name, class_name):
|
|
765
785
|
assert CLASS_NAME_SEPARATOR not in name
|
|
766
|
-
prefix = ''
|
|
767
786
|
if class_name:
|
|
768
787
|
assert CLASS_NAME_SEPARATOR not in class_name
|
|
769
788
|
prefix = f'x{CLASS_NAME_SEPARATOR}{class_name}{CLASS_NAME_SEPARATOR}'
|
|
@@ -779,12 +798,13 @@ def mangled_name_from_mutant_name(mutant_name):
|
|
|
779
798
|
|
|
780
799
|
def orig_function_and_class_names_from_key(mutant_name):
|
|
781
800
|
r = mangled_name_from_mutant_name(mutant_name)
|
|
801
|
+
_, _, r = r.rpartition('.')
|
|
782
802
|
class_name = None
|
|
783
803
|
if CLASS_NAME_SEPARATOR in r:
|
|
784
804
|
class_name = r[r.index(CLASS_NAME_SEPARATOR) + 1: r.rindex(CLASS_NAME_SEPARATOR)]
|
|
785
805
|
r = r[r.rindex(CLASS_NAME_SEPARATOR) + 1:]
|
|
786
806
|
else:
|
|
787
|
-
assert r.startswith('x_')
|
|
807
|
+
assert r.startswith('x_'), r
|
|
788
808
|
r = r[2:]
|
|
789
809
|
return r, class_name
|
|
790
810
|
|
|
@@ -837,6 +857,7 @@ def collect_stat(m: SourceFileMutationData):
|
|
|
837
857
|
for k in status_by_exit_code.values()
|
|
838
858
|
}
|
|
839
859
|
for k, v in m.exit_code_by_key.items():
|
|
860
|
+
# noinspection PyTypeChecker
|
|
840
861
|
r[status_by_exit_code[v].replace(' ', '_')] += 1
|
|
841
862
|
return Stat(
|
|
842
863
|
**r,
|
|
@@ -866,11 +887,10 @@ def print_stats(source_file_mutation_data_by_path, force_output=False):
|
|
|
866
887
|
|
|
867
888
|
def run_forced_fail(runner):
|
|
868
889
|
os.environ['MUTANT_UNDER_TEST'] = 'fail'
|
|
869
|
-
with CatchOutput(
|
|
890
|
+
with CatchOutput(spinner_title='Running forced fail test') as catcher:
|
|
870
891
|
try:
|
|
871
892
|
if runner.run_forced_fail() == 0:
|
|
872
|
-
catcher.
|
|
873
|
-
print('\n'.join(catcher.strings))
|
|
893
|
+
catcher.dump_output()
|
|
874
894
|
print("FAILED")
|
|
875
895
|
os._exit(1)
|
|
876
896
|
except MutmutProgrammaticFailException:
|
|
@@ -880,9 +900,8 @@ def run_forced_fail(runner):
|
|
|
880
900
|
|
|
881
901
|
|
|
882
902
|
class CatchOutput:
|
|
883
|
-
def __init__(self, callback=lambda s: None,
|
|
903
|
+
def __init__(self, callback=lambda s: None, spinner_title=None):
|
|
884
904
|
self.strings = []
|
|
885
|
-
self.show_spinner = show_spinner
|
|
886
905
|
self.spinner_title = spinner_title or ''
|
|
887
906
|
|
|
888
907
|
class StdOutRedirect(TextIOBase):
|
|
@@ -891,24 +910,29 @@ class CatchOutput:
|
|
|
891
910
|
|
|
892
911
|
def write(self, s):
|
|
893
912
|
callback(s)
|
|
894
|
-
if
|
|
913
|
+
if spinner_title:
|
|
895
914
|
print_status(spinner_title)
|
|
896
915
|
self.catcher.strings.append(s)
|
|
897
916
|
return len(s)
|
|
898
917
|
self.redirect = StdOutRedirect(self)
|
|
899
918
|
|
|
919
|
+
# noinspection PyMethodMayBeStatic
|
|
900
920
|
def stop(self):
|
|
901
921
|
sys.stdout = sys.__stdout__
|
|
902
922
|
sys.stderr = sys.__stderr__
|
|
903
923
|
|
|
904
924
|
def start(self):
|
|
925
|
+
if self.spinner_title:
|
|
926
|
+
print_status(self.spinner_title)
|
|
905
927
|
sys.stdout = self.redirect
|
|
906
928
|
sys.stderr = self.redirect
|
|
929
|
+
if mutmut.config.debug:
|
|
930
|
+
self.stop()
|
|
907
931
|
|
|
908
932
|
def dump_output(self):
|
|
909
933
|
self.stop()
|
|
910
|
-
for
|
|
911
|
-
|
|
934
|
+
for line in self.strings:
|
|
935
|
+
print(line, end='')
|
|
912
936
|
|
|
913
937
|
def __enter__(self):
|
|
914
938
|
self.start()
|
|
@@ -916,7 +940,7 @@ class CatchOutput:
|
|
|
916
940
|
|
|
917
941
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
918
942
|
self.stop()
|
|
919
|
-
if self.
|
|
943
|
+
if self.spinner_title:
|
|
920
944
|
print()
|
|
921
945
|
|
|
922
946
|
|
|
@@ -925,6 +949,8 @@ class Config:
|
|
|
925
949
|
also_copy: List[Path]
|
|
926
950
|
do_not_mutate: List[str]
|
|
927
951
|
max_stack_depth: int
|
|
952
|
+
debug: bool
|
|
953
|
+
paths_to_mutate: List[Path]
|
|
928
954
|
|
|
929
955
|
def should_ignore_for_mutation(self, path):
|
|
930
956
|
if not str(path).endswith('.py'):
|
|
@@ -935,36 +961,67 @@ class Config:
|
|
|
935
961
|
return False
|
|
936
962
|
|
|
937
963
|
|
|
938
|
-
|
|
939
|
-
|
|
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
|
+
|
|
940
987
|
config_parser = ConfigParser()
|
|
941
988
|
config_parser.read('setup.cfg')
|
|
942
989
|
|
|
943
990
|
def s(key, default):
|
|
944
991
|
try:
|
|
945
|
-
|
|
992
|
+
result = config_parser.get('mutmut', key)
|
|
946
993
|
except (NoOptionError, NoSectionError):
|
|
947
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()
|
|
948
1008
|
|
|
949
1009
|
mutmut.config = Config(
|
|
950
|
-
do_not_mutate=[
|
|
951
|
-
x
|
|
952
|
-
for x in s('do_not_mutate', '').split('\n')
|
|
953
|
-
if x
|
|
954
|
-
],
|
|
1010
|
+
do_not_mutate=s('do_not_mutate', []),
|
|
955
1011
|
also_copy=[
|
|
956
1012
|
Path(y)
|
|
957
|
-
for y in [
|
|
958
|
-
x
|
|
959
|
-
for x in s('also_copy', '').split('\n')
|
|
960
|
-
if x
|
|
961
|
-
]
|
|
1013
|
+
for y in s('also_copy', [])
|
|
962
1014
|
]+[
|
|
963
1015
|
Path('tests/'),
|
|
964
1016
|
Path('test/'),
|
|
965
1017
|
Path('tests.py'),
|
|
966
1018
|
],
|
|
967
|
-
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()
|
|
968
1025
|
)
|
|
969
1026
|
|
|
970
1027
|
|
|
@@ -979,7 +1036,9 @@ def run_stats_collection(runner, tests=None):
|
|
|
979
1036
|
|
|
980
1037
|
os.environ['MUTANT_UNDER_TEST'] = 'stats'
|
|
981
1038
|
os.environ['PY_IGNORE_IMPORTMISMATCH'] = '1'
|
|
982
|
-
|
|
1039
|
+
start_cpu_time = process_time()
|
|
1040
|
+
|
|
1041
|
+
with CatchOutput(spinner_title='Running stats') as output_catcher:
|
|
983
1042
|
collect_stats_exit_code = runner.run_stats(tests=tests)
|
|
984
1043
|
if collect_stats_exit_code != 0:
|
|
985
1044
|
output_catcher.dump_output()
|
|
@@ -987,8 +1046,10 @@ def run_stats_collection(runner, tests=None):
|
|
|
987
1046
|
exit(1)
|
|
988
1047
|
|
|
989
1048
|
print(' done')
|
|
1049
|
+
if not tests: # again, meaning all
|
|
1050
|
+
mutmut.stats_time = process_time() - start_cpu_time
|
|
990
1051
|
|
|
991
|
-
if not
|
|
1052
|
+
if not collected_test_names():
|
|
992
1053
|
print('failed to collect stats, no active tests found')
|
|
993
1054
|
exit(1)
|
|
994
1055
|
|
|
@@ -1003,7 +1064,7 @@ def collect_or_load_stats(runner):
|
|
|
1003
1064
|
run_stats_collection(runner)
|
|
1004
1065
|
else:
|
|
1005
1066
|
# Run incremental stats
|
|
1006
|
-
with CatchOutput(
|
|
1067
|
+
with CatchOutput(spinner_title='Listing all tests') as output_catcher:
|
|
1007
1068
|
os.environ['MUTANT_UNDER_TEST'] = 'list_all_tests'
|
|
1008
1069
|
try:
|
|
1009
1070
|
all_tests_result = runner.list_all_tests()
|
|
@@ -1017,6 +1078,7 @@ def collect_or_load_stats(runner):
|
|
|
1017
1078
|
new_tests = all_tests_result.new_tests()
|
|
1018
1079
|
|
|
1019
1080
|
if new_tests:
|
|
1081
|
+
print(f'Found {len(new_tests)} new tests, rerunning stats collection')
|
|
1020
1082
|
run_stats_collection(runner, tests=new_tests)
|
|
1021
1083
|
|
|
1022
1084
|
|
|
@@ -1028,6 +1090,7 @@ def load_stats():
|
|
|
1028
1090
|
for k, v in data.pop('tests_by_mangled_function_name').items():
|
|
1029
1091
|
mutmut.tests_by_mangled_function_name[k] |= set(v)
|
|
1030
1092
|
mutmut.duration_by_test = data.pop('duration_by_test')
|
|
1093
|
+
mutmut.stats_time = data.pop('stats_time')
|
|
1031
1094
|
assert not data, data
|
|
1032
1095
|
did_load = True
|
|
1033
1096
|
except (FileNotFoundError, JSONDecodeError):
|
|
@@ -1040,6 +1103,7 @@ def save_stats():
|
|
|
1040
1103
|
json.dump(dict(
|
|
1041
1104
|
tests_by_mangled_function_name={k: list(v) for k, v in mutmut.tests_by_mangled_function_name.items()},
|
|
1042
1105
|
duration_by_test=mutmut.duration_by_test,
|
|
1106
|
+
stats_time=mutmut.stats_time,
|
|
1043
1107
|
), f, indent=4)
|
|
1044
1108
|
|
|
1045
1109
|
|
|
@@ -1118,6 +1182,23 @@ def stop_all_children(mutants):
|
|
|
1118
1182
|
m.stop_children()
|
|
1119
1183
|
|
|
1120
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
|
+
|
|
1121
1202
|
@cli.command()
|
|
1122
1203
|
@click.option('--max-children', type=int)
|
|
1123
1204
|
@click.argument('mutant_names', required=False, nargs=-1)
|
|
@@ -1126,13 +1207,15 @@ def run(mutant_names, *, max_children):
|
|
|
1126
1207
|
|
|
1127
1208
|
# TODO: run no-ops once in a while to detect if we get false negatives
|
|
1128
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!
|
|
1129
|
-
|
|
1130
|
-
start = datetime.now()
|
|
1131
|
-
print('generating mutants')
|
|
1132
1210
|
os.environ['MUTANT_UNDER_TEST'] = 'mutant_generation'
|
|
1133
1211
|
read_config()
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
+
|
|
1136
1219
|
time = datetime.now() - start
|
|
1137
1220
|
print(f' done in {round(time.total_seconds()*1000)}ms', )
|
|
1138
1221
|
|
|
@@ -1157,13 +1240,13 @@ def run(mutant_names, *, max_children):
|
|
|
1157
1240
|
mutants, source_file_mutation_data_by_path = collect_source_file_mutation_data(mutant_names=mutant_names)
|
|
1158
1241
|
|
|
1159
1242
|
os.environ['MUTANT_UNDER_TEST'] = ''
|
|
1160
|
-
with CatchOutput(
|
|
1243
|
+
with CatchOutput(spinner_title='Running clean tests') as output_catcher:
|
|
1161
1244
|
tests = tests_for_mutant_names(mutant_names)
|
|
1162
1245
|
|
|
1163
1246
|
clean_test_exit_code = runner.run_tests(mutant_name=None, tests=tests)
|
|
1164
1247
|
if clean_test_exit_code != 0:
|
|
1165
1248
|
output_catcher.dump_output()
|
|
1166
|
-
print('
|
|
1249
|
+
print('Failed to run clean test')
|
|
1167
1250
|
exit(1)
|
|
1168
1251
|
print(' done')
|
|
1169
1252
|
|
|
@@ -1173,7 +1256,10 @@ def run(mutant_names, *, max_children):
|
|
|
1173
1256
|
runner.prepare_main_test_run()
|
|
1174
1257
|
|
|
1175
1258
|
def read_one_child_exit_status():
|
|
1176
|
-
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)
|
|
1177
1263
|
source_file_mutation_data_by_pid[pid].register_result(pid=pid, exit_code=exit_code)
|
|
1178
1264
|
|
|
1179
1265
|
source_file_mutation_data_by_pid: Dict[int, SourceFileMutationData] = {} # many pids map to one MutationData
|
|
@@ -1192,6 +1278,16 @@ def run(mutant_names, *, max_children):
|
|
|
1192
1278
|
try:
|
|
1193
1279
|
print('Running mutation testing')
|
|
1194
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
|
|
1195
1291
|
for m, mutant_name, result in mutants:
|
|
1196
1292
|
print_stats(source_file_mutation_data_by_path)
|
|
1197
1293
|
|
|
@@ -1220,6 +1316,10 @@ def run(mutant_names, *, max_children):
|
|
|
1220
1316
|
if not tests:
|
|
1221
1317
|
os._exit(33)
|
|
1222
1318
|
|
|
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))
|
|
1322
|
+
|
|
1223
1323
|
with CatchOutput():
|
|
1224
1324
|
result = runner.run_tests(mutant_name=mutant_name, tests=tests)
|
|
1225
1325
|
|
|
@@ -1230,7 +1330,7 @@ def run(mutant_names, *, max_children):
|
|
|
1230
1330
|
else:
|
|
1231
1331
|
# in the parent
|
|
1232
1332
|
source_file_mutation_data_by_pid[pid] = m
|
|
1233
|
-
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)
|
|
1234
1334
|
running_children += 1
|
|
1235
1335
|
|
|
1236
1336
|
if running_children >= max_children:
|
|
@@ -1246,7 +1346,7 @@ def run(mutant_names, *, max_children):
|
|
|
1246
1346
|
except ChildProcessError:
|
|
1247
1347
|
pass
|
|
1248
1348
|
except KeyboardInterrupt:
|
|
1249
|
-
print('
|
|
1349
|
+
print('Stopping...')
|
|
1250
1350
|
stop_all_children(mutants)
|
|
1251
1351
|
|
|
1252
1352
|
t = datetime.now() - start
|
|
@@ -1427,7 +1527,9 @@ def browse():
|
|
|
1427
1527
|
from textual.containers import Container
|
|
1428
1528
|
from textual.widgets import Footer
|
|
1429
1529
|
from textual.widgets import DataTable
|
|
1430
|
-
from textual.widgets import
|
|
1530
|
+
from textual.widgets import Static
|
|
1531
|
+
from textual.widget import Widget
|
|
1532
|
+
from rich.syntax import Syntax
|
|
1431
1533
|
|
|
1432
1534
|
class ResultBrowser(App):
|
|
1433
1535
|
loading_id = None
|
|
@@ -1443,7 +1545,7 @@ def browse():
|
|
|
1443
1545
|
columns = [
|
|
1444
1546
|
('path', 'Path'),
|
|
1445
1547
|
] + [
|
|
1446
|
-
(status, emoji)
|
|
1548
|
+
(status, Text(emoji, justify='right'))
|
|
1447
1549
|
for status, emoji in emoji_by_status.items()
|
|
1448
1550
|
]
|
|
1449
1551
|
|
|
@@ -1454,17 +1556,20 @@ def browse():
|
|
|
1454
1556
|
with Container(classes='container'):
|
|
1455
1557
|
yield DataTable(id='files')
|
|
1456
1558
|
yield DataTable(id='mutants')
|
|
1457
|
-
|
|
1559
|
+
with Widget(id="diff_view_widget"):
|
|
1560
|
+
yield Static(id='diff_view')
|
|
1458
1561
|
yield Footer()
|
|
1459
1562
|
|
|
1460
1563
|
def on_mount(self):
|
|
1461
1564
|
# files table
|
|
1565
|
+
# noinspection PyTypeChecker
|
|
1462
1566
|
files_table: DataTable = self.query_one('#files')
|
|
1463
1567
|
files_table.cursor_type = 'row'
|
|
1464
1568
|
for key, label in self.columns:
|
|
1465
1569
|
files_table.add_column(key=key, label=label)
|
|
1466
1570
|
|
|
1467
1571
|
# mutants table
|
|
1572
|
+
# noinspection PyTypeChecker
|
|
1468
1573
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1469
1574
|
mutants_table.cursor_type = 'row'
|
|
1470
1575
|
mutants_table.add_columns('name', 'status')
|
|
@@ -1483,17 +1588,21 @@ def browse():
|
|
|
1483
1588
|
source_file_mutation_data.load()
|
|
1484
1589
|
stat = collect_stat(source_file_mutation_data)
|
|
1485
1590
|
|
|
1486
|
-
self.source_file_mutation_data_and_stat_by_path[p] = source_file_mutation_data, stat
|
|
1591
|
+
self.source_file_mutation_data_and_stat_by_path[str(p)] = source_file_mutation_data, stat
|
|
1487
1592
|
|
|
1488
1593
|
def populate_files_table(self):
|
|
1594
|
+
# noinspection PyTypeChecker
|
|
1489
1595
|
files_table: DataTable = self.query_one('#files')
|
|
1490
1596
|
# TODO: restore selection
|
|
1491
1597
|
selected_row = files_table.cursor_row
|
|
1492
1598
|
files_table.clear()
|
|
1493
1599
|
|
|
1494
1600
|
for p, (source_file_mutation_data, stat) in sorted(self.source_file_mutation_data_and_stat_by_path.items()):
|
|
1495
|
-
row = [p] + [
|
|
1496
|
-
|
|
1601
|
+
row = [p] + [
|
|
1602
|
+
Text(str(getattr(stat, k.replace(' ', '_'))), justify="right")
|
|
1603
|
+
for k, _ in self.columns[1:]
|
|
1604
|
+
]
|
|
1605
|
+
files_table.add_row(*row, key=str(p))
|
|
1497
1606
|
|
|
1498
1607
|
files_table.move_cursor(row=selected_row)
|
|
1499
1608
|
|
|
@@ -1501,6 +1610,7 @@ def browse():
|
|
|
1501
1610
|
if not event.row_key or not event.row_key.value:
|
|
1502
1611
|
return
|
|
1503
1612
|
if event.data_table.id == 'files':
|
|
1613
|
+
# noinspection PyTypeChecker
|
|
1504
1614
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1505
1615
|
mutants_table.clear()
|
|
1506
1616
|
source_file_mutation_data, stat = self.source_file_mutation_data_and_stat_by_path[event.row_key.value]
|
|
@@ -1511,11 +1621,12 @@ def browse():
|
|
|
1511
1621
|
mutants_table.add_row(k, emoji_by_status[status], key=k)
|
|
1512
1622
|
else:
|
|
1513
1623
|
assert event.data_table.id == 'mutants'
|
|
1514
|
-
|
|
1624
|
+
# noinspection PyTypeChecker
|
|
1625
|
+
diff_view: Static = self.query_one('#diff_view')
|
|
1515
1626
|
if event.row_key.value is None:
|
|
1516
|
-
diff_view.
|
|
1627
|
+
diff_view.update('')
|
|
1517
1628
|
else:
|
|
1518
|
-
diff_view.
|
|
1629
|
+
diff_view.update('<loading...>')
|
|
1519
1630
|
self.loading_id = event.row_key.value
|
|
1520
1631
|
|
|
1521
1632
|
def load_thread():
|
|
@@ -1523,9 +1634,9 @@ def browse():
|
|
|
1523
1634
|
try:
|
|
1524
1635
|
d = get_diff_for_mutant(event.row_key.value)
|
|
1525
1636
|
if event.row_key.value == self.loading_id:
|
|
1526
|
-
diff_view.
|
|
1637
|
+
diff_view.update(Syntax(d, "diff"))
|
|
1527
1638
|
except Exception as e:
|
|
1528
|
-
diff_view.
|
|
1639
|
+
diff_view.update(f"<{type(e)} {e}>")
|
|
1529
1640
|
|
|
1530
1641
|
t = Thread(target=load_thread)
|
|
1531
1642
|
t.start()
|
|
@@ -1541,6 +1652,7 @@ def browse():
|
|
|
1541
1652
|
self.populate_files_table()
|
|
1542
1653
|
|
|
1543
1654
|
def get_mutant_name_from_selection(self):
|
|
1655
|
+
# noinspection PyTypeChecker
|
|
1544
1656
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1545
1657
|
if mutants_table.cursor_row is None:
|
|
1546
1658
|
return
|
|
@@ -1558,6 +1670,7 @@ def browse():
|
|
|
1558
1670
|
|
|
1559
1671
|
def action_apply_mutant(self):
|
|
1560
1672
|
read_config()
|
|
1673
|
+
# noinspection PyTypeChecker
|
|
1561
1674
|
mutants_table: DataTable = self.query_one('#mutants')
|
|
1562
1675
|
if mutants_table.cursor_row is None:
|
|
1563
1676
|
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.1
|
|
3
|
+
Version: 3.2.1
|
|
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
|