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.
Files changed (26) hide show
  1. {mutmut-3.2.0 → mutmut-3.2.2}/HISTORY.rst +18 -0
  2. {mutmut-3.2.0 → mutmut-3.2.2}/PKG-INFO +2 -1
  3. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/.DS_Store +0 -0
  4. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__init__.py +3 -3
  5. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__main__.py +173 -78
  6. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
  7. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
  8. mutmut-3.2.2/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
  9. mutmut-3.2.2/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
  10. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/PKG-INFO +2 -1
  11. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/requires.txt +3 -0
  12. {mutmut-3.2.0 → mutmut-3.2.2}/requirements.txt +1 -0
  13. mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
  14. mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
  15. {mutmut-3.2.0 → mutmut-3.2.2}/AUTHORS.rst +0 -0
  16. {mutmut-3.2.0 → mutmut-3.2.2}/LICENSE +0 -0
  17. {mutmut-3.2.0 → mutmut-3.2.2}/MANIFEST.in +0 -0
  18. {mutmut-3.2.0 → mutmut-3.2.2}/README.rst +0 -0
  19. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut/result_browser_layout.tcss +0 -0
  20. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/SOURCES.txt +0 -0
  21. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/dependency_links.txt +0 -0
  22. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/entry_points.txt +0 -0
  23. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/not-zip-safe +0 -0
  24. {mutmut-3.2.0 → mutmut-3.2.2}/mutmut.egg-info/top_level.txt +0 -0
  25. {mutmut-3.2.0 → mutmut-3.2.2}/setup.cfg +0 -0
  26. {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.0
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
  ===============================
@@ -6,7 +6,7 @@ from parso.python.tree import (
6
6
  Number,
7
7
  )
8
8
 
9
- __version__ = '3.2.0'
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
- # 'break': 'continue',
169
- # 'continue': 'break',
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 isdir
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 process_time
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 on the command line using --paths-to-mutate, '
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
- paths = [guess_paths_to_mutate()]
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]] = (0xFF00 & exit_code) >> 8 # The high byte contains the exit code
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('running hammett stats...')
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(show_spinner=True, spinner_title='running forced fail test') as catcher:
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, show_spinner=False, spinner_title=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 show_spinner:
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.show_spinner:
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 l in self.strings:
920
- print(l, end='')
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.show_spinner:
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
- @lru_cache()
948
- def read_config():
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
- return config_parser.get('mutmut', key)
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=int(s('max_stack_depth', '-1'))
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(show_spinner=True, spinner_title='running stats') as output_catcher:
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(show_spinner=True, spinner_title='collecting stats') as output_catcher:
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
- create_mutants()
1150
- copy_also_copy_files()
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(show_spinner=True, spinner_title='running clean tests') as output_catcher:
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('failed to run clean test')
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, exit_code = os.wait()
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 = sum(mutmut.duration_by_test[test_name] for test_name in tests) + 1
1239
- resource.setrlimit(resource.RLIMIT_CPU, (ceil(estimated_time_of_tests * 2), ceil(estimated_time_of_tests * 2)))
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('stopping')
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] + [getattr(stat, k.replace(' ', '_')) for k, _ in self.columns[1:]]
1517
- files_table.add_row(*row, key=p)
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
- diff_view = self.query_one('#diff_view')
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.2.0
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
  ===============================
@@ -3,3 +3,6 @@ click
3
3
  junit-xml==1.8
4
4
  setproctitle
5
5
  textual
6
+
7
+ [:python_version < "3.11"]
8
+ toml>=0.10.2
@@ -3,3 +3,4 @@ click
3
3
  junit-xml==1.8
4
4
  setproctitle
5
5
  textual
6
+ toml>=0.10.2; python_version < '3.11'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes