mutmut 3.1.0__tar.gz → 3.2.0__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.1.0 → mutmut-3.2.0}/AUTHORS.rst +1 -0
  2. {mutmut-3.1.0 → mutmut-3.2.0}/HISTORY.rst +14 -0
  3. {mutmut-3.1.0 → mutmut-3.2.0}/PKG-INFO +1 -1
  4. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/__init__.py +10 -1
  5. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/__main__.py +57 -36
  6. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
  7. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
  8. mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
  9. mutmut-3.2.0/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
  10. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/result_browser_layout.tcss +8 -0
  11. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/PKG-INFO +1 -1
  12. mutmut-3.1.0/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
  13. mutmut-3.1.0/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
  14. {mutmut-3.1.0 → mutmut-3.2.0}/LICENSE +0 -0
  15. {mutmut-3.1.0 → mutmut-3.2.0}/MANIFEST.in +0 -0
  16. {mutmut-3.1.0 → mutmut-3.2.0}/README.rst +0 -0
  17. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut/.DS_Store +0 -0
  18. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/SOURCES.txt +0 -0
  19. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/dependency_links.txt +0 -0
  20. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/entry_points.txt +0 -0
  21. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/not-zip-safe +0 -0
  22. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/requires.txt +0 -0
  23. {mutmut-3.1.0 → mutmut-3.2.0}/mutmut.egg-info/top_level.txt +0 -0
  24. {mutmut-3.1.0 → mutmut-3.2.0}/requirements.txt +0 -0
  25. {mutmut-3.1.0 → mutmut-3.2.0}/setup.cfg +0 -0
  26. {mutmut-3.1.0 → mutmut-3.2.0}/setup.py +0 -0
@@ -20,3 +20,4 @@ Credits
20
20
  * Emil Stenström <emil@emilstenstrom.se>
21
21
  * Roxane Bellott <roxane.bellot@gmail.com>
22
22
  * Tomáš Chvátal <tchvatal@suse.com>
23
+ * Frank Hoffmann <15r10nk-git@polarbit.de>
@@ -1,6 +1,20 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 3.2.0
5
+ ~~~~~
6
+
7
+ * Timeouts for mutants implemented.
8
+
9
+ * Browser: syntax highlighting for diff view
10
+
11
+ * More fixes for generators.
12
+
13
+ * Fix for `src`-style layout of projects.
14
+
15
+ * Fixed bug where mutmut would recollect all tests on every run, slowing down startup.
16
+
17
+
4
18
  3.1.0
5
19
  ~~~~~
6
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: mutation testing for Python 3
5
5
  Home-page: https://github.com/boxed/mutmut
6
6
  Author: Anders Hovmöller
@@ -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.0'
9
+ __version__ = '3.2.0'
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
@@ -25,6 +25,7 @@ from functools import lru_cache
25
25
  from hashlib import md5
26
26
  from io import TextIOBase
27
27
  from json import JSONDecodeError
28
+ from math import ceil
28
29
  from os import (
29
30
  makedirs,
30
31
  walk,
@@ -36,7 +37,10 @@ from textwrap import (
36
37
  dedent,
37
38
  indent,
38
39
  )
40
+ import resource
41
+
39
42
  from threading import Thread
43
+ from time import process_time
40
44
  from typing import (
41
45
  Dict,
42
46
  List,
@@ -51,7 +55,6 @@ from setproctitle import setproctitle
51
55
 
52
56
  import mutmut
53
57
 
54
- mutmut.duration_by_test = {}
55
58
 
56
59
  # Document: surviving mutants are retested when you ask mutmut to retest them, interactively in the UI or via command line
57
60
 
@@ -71,9 +74,6 @@ NEVER_MUTATE_FUNCTION_NAMES = {'__getattribute__', '__setattr__'}
71
74
  NEVER_MUTATE_FUNCTION_CALLS = {'isinstance', 'len'}
72
75
  CLASS_NAME_SEPARATOR = 'ǁ'
73
76
 
74
- mutmut._stats = set()
75
- mutmut.tests_by_mangled_function_name = defaultdict(set)
76
-
77
77
 
78
78
  status_by_exit_code = {
79
79
  1: 'killed',
@@ -86,6 +86,7 @@ status_by_exit_code = {
86
86
  34: 'skipped',
87
87
  35: 'suspicious',
88
88
  36: 'timeout',
89
+ 152: 'timeout', # SIGXCPU
89
90
  }
90
91
 
91
92
  emoji_by_status = {
@@ -189,18 +190,18 @@ def _mutmut_trampoline(orig, mutants, *args, **kwargs):
189
190
  elif mutant_under_test == 'stats':
190
191
  from mutmut.__main__ import record_trampoline_hit
191
192
  record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
192
- ret orig(*args, **kwargs)
193
- return # for the yield case
193
+ result = orig(*args, **kwargs)
194
+ return result # for the yield case
194
195
  prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
195
196
  if not mutant_under_test.startswith(prefix):
196
- ret orig(*args, **kwargs)
197
- return # for the yield case
197
+ result = orig(*args, **kwargs)
198
+ return result # for the yield case
198
199
  mutant_name = mutant_under_test.rpartition('.')[-1]
199
- ret mutants[mutant_name](*args, **kwargs)
200
+ result = mutants[mutant_name](*args, **kwargs)
201
+ return result
200
202
 
201
203
  """
202
- yield_from_trampoline_impl = trampoline_impl.replace('ret ', 'yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
203
- trampoline_impl = trampoline_impl.replace('ret ', 'return ')
204
+ yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
204
205
 
205
206
 
206
207
  def create_mutants():
@@ -257,7 +258,8 @@ def create_mutants_for_file(filename, output_path):
257
258
  exit(1)
258
259
 
259
260
  source_file_mutation_data = SourceFileMutationData(path=filename)
260
- module_name = str(filename)[:-len(filename.suffix)].replace(os.sep, '.')
261
+ module_name = strip_prefix(str(filename)[:-len(filename.suffix)].replace(os.sep, '.'), prefix='src.')
262
+
261
263
  source_file_mutation_data.exit_code_by_key = {
262
264
  '.'.join([module_name, x]).replace('.__init__.', '.'): None
263
265
  for x in mutant_names
@@ -314,17 +316,18 @@ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
314
316
  access_suffix = '")'
315
317
 
316
318
  if is_generator:
317
- return_or_yield_statement = 'yield from'
319
+ yield_statement = 'yield from ' # note the space at the end!
318
320
  trampoline_name = '_mutmut_yield_from_trampoline'
319
321
  else:
320
- return_or_yield_statement = 'return'
322
+ yield_statement = ''
321
323
  trampoline_name = '_mutmut_trampoline'
322
324
 
323
325
  return f"""
324
326
  {mutants_dict}
325
327
 
326
328
  def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
327
- {return_or_yield_statement} {trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, *args, **kwargs)
329
+ result = {yield_statement}{trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, *args, **kwargs)
330
+ return result
328
331
 
329
332
  {orig_name}.__signature__ = _mutmut_signature({mangled_name}__mutmut_orig)
330
333
  {mangled_name}__mutmut_orig.__name__ = '{mangled_name}'
@@ -655,28 +658,32 @@ def change_cwd(path):
655
658
  os.chdir(old_cwd)
656
659
 
657
660
 
661
+ 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
+ }
667
+
668
+
658
669
  class ListAllTestsResult:
659
670
  def __init__(self, *, ids):
660
671
  assert isinstance(ids, set)
661
672
  self.ids = ids
662
673
 
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
674
  def clear_out_obsolete_test_names(self):
675
+ count_before = len(mutmut.tests_by_mangled_function_name)
672
676
  mutmut.tests_by_mangled_function_name = {
673
- k: {test_name for test_name in test_names if test_name not in self.ids}
677
+ k: {test_name for test_name in test_names if test_name in self.ids}
674
678
  for k, test_names in mutmut.tests_by_mangled_function_name.items()
675
679
  }
676
- save_stats()
680
+ count_after = len(mutmut.tests_by_mangled_function_name)
681
+ if count_before != count_after:
682
+ print(f'Removed {count_before - count_after} obsolete test names')
683
+ save_stats()
677
684
 
678
685
  def new_tests(self):
679
- return self.ids - self.old_tests()
686
+ return self.ids - collected_test_names()
680
687
 
681
688
 
682
689
  class PytestRunner(TestRunner):
@@ -779,12 +786,13 @@ def mangled_name_from_mutant_name(mutant_name):
779
786
 
780
787
  def orig_function_and_class_names_from_key(mutant_name):
781
788
  r = mangled_name_from_mutant_name(mutant_name)
789
+ _, _, r = r.rpartition('.')
782
790
  class_name = None
783
791
  if CLASS_NAME_SEPARATOR in r:
784
792
  class_name = r[r.index(CLASS_NAME_SEPARATOR) + 1: r.rindex(CLASS_NAME_SEPARATOR)]
785
793
  r = r[r.rindex(CLASS_NAME_SEPARATOR) + 1:]
786
794
  else:
787
- assert r.startswith('x_')
795
+ assert r.startswith('x_'), r
788
796
  r = r[2:]
789
797
  return r, class_name
790
798
 
@@ -869,8 +877,7 @@ def run_forced_fail(runner):
869
877
  with CatchOutput(show_spinner=True, spinner_title='running forced fail test') as catcher:
870
878
  try:
871
879
  if runner.run_forced_fail() == 0:
872
- catcher.stop()
873
- print('\n'.join(catcher.strings))
880
+ catcher.dump_output()
874
881
  print("FAILED")
875
882
  os._exit(1)
876
883
  except MutmutProgrammaticFailException:
@@ -902,6 +909,8 @@ class CatchOutput:
902
909
  sys.stderr = sys.__stderr__
903
910
 
904
911
  def start(self):
912
+ if self.show_spinner:
913
+ print_status(self.spinner_title)
905
914
  sys.stdout = self.redirect
906
915
  sys.stderr = self.redirect
907
916
 
@@ -979,6 +988,8 @@ def run_stats_collection(runner, tests=None):
979
988
 
980
989
  os.environ['MUTANT_UNDER_TEST'] = 'stats'
981
990
  os.environ['PY_IGNORE_IMPORTMISMATCH'] = '1'
991
+ start_cpu_time = process_time()
992
+
982
993
  with CatchOutput(show_spinner=True, spinner_title='running stats') as output_catcher:
983
994
  collect_stats_exit_code = runner.run_stats(tests=tests)
984
995
  if collect_stats_exit_code != 0:
@@ -987,8 +998,10 @@ def run_stats_collection(runner, tests=None):
987
998
  exit(1)
988
999
 
989
1000
  print(' done')
1001
+ if not tests: # again, meaning all
1002
+ mutmut.stats_time = process_time() - start_cpu_time
990
1003
 
991
- if not mutmut.tests_by_mangled_function_name:
1004
+ if not collected_test_names():
992
1005
  print('failed to collect stats, no active tests found')
993
1006
  exit(1)
994
1007
 
@@ -1028,6 +1041,7 @@ def load_stats():
1028
1041
  for k, v in data.pop('tests_by_mangled_function_name').items():
1029
1042
  mutmut.tests_by_mangled_function_name[k] |= set(v)
1030
1043
  mutmut.duration_by_test = data.pop('duration_by_test')
1044
+ mutmut.stats_time = data.pop('stats_time')
1031
1045
  assert not data, data
1032
1046
  did_load = True
1033
1047
  except (FileNotFoundError, JSONDecodeError):
@@ -1040,6 +1054,7 @@ def save_stats():
1040
1054
  json.dump(dict(
1041
1055
  tests_by_mangled_function_name={k: list(v) for k, v in mutmut.tests_by_mangled_function_name.items()},
1042
1056
  duration_by_test=mutmut.duration_by_test,
1057
+ stats_time=mutmut.stats_time,
1043
1058
  ), f, indent=4)
1044
1059
 
1045
1060
 
@@ -1220,6 +1235,9 @@ def run(mutant_names, *, max_children):
1220
1235
  if not tests:
1221
1236
  os._exit(33)
1222
1237
 
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)))
1240
+
1223
1241
  with CatchOutput():
1224
1242
  result = runner.run_tests(mutant_name=mutant_name, tests=tests)
1225
1243
 
@@ -1427,7 +1445,9 @@ def browse():
1427
1445
  from textual.containers import Container
1428
1446
  from textual.widgets import Footer
1429
1447
  from textual.widgets import DataTable
1430
- from textual.widgets import TextArea
1448
+ from textual.widgets import Static
1449
+ from textual.widget import Widget
1450
+ from rich.syntax import Syntax
1431
1451
 
1432
1452
  class ResultBrowser(App):
1433
1453
  loading_id = None
@@ -1454,7 +1474,8 @@ def browse():
1454
1474
  with Container(classes='container'):
1455
1475
  yield DataTable(id='files')
1456
1476
  yield DataTable(id='mutants')
1457
- yield TextArea(id='diff_view')
1477
+ with Widget(id="diff_view_widget"):
1478
+ yield Static(id='diff_view')
1458
1479
  yield Footer()
1459
1480
 
1460
1481
  def on_mount(self):
@@ -1513,9 +1534,9 @@ def browse():
1513
1534
  assert event.data_table.id == 'mutants'
1514
1535
  diff_view = self.query_one('#diff_view')
1515
1536
  if event.row_key.value is None:
1516
- diff_view.text = ''
1537
+ diff_view.update('')
1517
1538
  else:
1518
- diff_view.text = '<loading...>'
1539
+ diff_view.update('<loading...>')
1519
1540
  self.loading_id = event.row_key.value
1520
1541
 
1521
1542
  def load_thread():
@@ -1523,9 +1544,9 @@ def browse():
1523
1544
  try:
1524
1545
  d = get_diff_for_mutant(event.row_key.value)
1525
1546
  if event.row_key.value == self.loading_id:
1526
- diff_view.text = d
1547
+ diff_view.update(Syntax(d, "diff"))
1527
1548
  except Exception as e:
1528
- diff_view.text = f'<{e}>'
1549
+ diff_view.update(f"<{type(e)} {e}>")
1529
1550
 
1530
1551
  t = Thread(target=load_thread)
1531
1552
  t.start()
@@ -24,3 +24,11 @@ DataTable:focus .datatable--cursor {
24
24
  color: black;
25
25
  background: orange;
26
26
  }
27
+
28
+ #diff_view_widget {
29
+ height: 50%;
30
+ overflow-y: scroll;
31
+ }
32
+
33
+ #diff_view {
34
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.1.0
3
+ Version: 3.2.0
4
4
  Summary: mutation testing for Python 3
5
5
  Home-page: https://github.com/boxed/mutmut
6
6
  Author: Anders Hovmöller
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes