mutmut 3.0.5__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.
@@ -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,32 @@
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
+
18
+ 3.1.0
19
+ ~~~~~
20
+
21
+ * Correctly handle mutation for generator functions (`yield`).
22
+
23
+ * Fixed so that `from __future__` lines are always first.
24
+
25
+ * If no stats are collected exit directly, as that is a breaking error for mutation testing.
26
+
27
+ * Changed name mangling to make mutants less likely to trigger name-based python magic, like in pytest where functions named `test_*` have special meaning.
28
+
29
+
4
30
  3.0.5
5
31
  ~~~~~
6
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.0.5
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.0.5'
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,14 +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
- return orig(*args, **kwargs)
193
+ result = orig(*args, **kwargs)
194
+ return result # for the yield case
193
195
  prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
194
196
  if not mutant_under_test.startswith(prefix):
195
- return orig(*args, **kwargs)
197
+ result = orig(*args, **kwargs)
198
+ return result # for the yield case
196
199
  mutant_name = mutant_under_test.rpartition('.')[-1]
197
- return mutants[mutant_name](*args, **kwargs)
200
+ result = mutants[mutant_name](*args, **kwargs)
201
+ return result
198
202
 
199
203
  """
204
+ yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
200
205
 
201
206
 
202
207
  def create_mutants():
@@ -253,7 +258,8 @@ def create_mutants_for_file(filename, output_path):
253
258
  exit(1)
254
259
 
255
260
  source_file_mutation_data = SourceFileMutationData(path=filename)
256
- 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
+
257
263
  source_file_mutation_data.exit_code_by_key = {
258
264
  '.'.join([module_name, x]).replace('.__init__.', '.'): None
259
265
  for x in mutant_names
@@ -297,7 +303,7 @@ def write_all_mutants_to_file(*, out, source, filename):
297
303
  return mutant_names, hash_by_function_name
298
304
 
299
305
 
300
- def build_trampoline(orig_name, mutants, class_name=None):
306
+ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
301
307
  assert orig_name not in NEVER_MUTATE_FUNCTION_NAMES
302
308
 
303
309
  mangled_name = mangle_function_name(name=orig_name, class_name=class_name)
@@ -309,11 +315,19 @@ def build_trampoline(orig_name, mutants, class_name=None):
309
315
  access_prefix = f'object.__getattribute__(self, "'
310
316
  access_suffix = '")'
311
317
 
318
+ if is_generator:
319
+ yield_statement = 'yield from ' # note the space at the end!
320
+ trampoline_name = '_mutmut_yield_from_trampoline'
321
+ else:
322
+ yield_statement = ''
323
+ trampoline_name = '_mutmut_trampoline'
324
+
312
325
  return f"""
313
326
  {mutants_dict}
314
327
 
315
328
  def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
316
- return _mutmut_trampoline({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
317
331
 
318
332
  {orig_name}.__signature__ = _mutmut_signature({mangled_name}__mutmut_orig)
319
333
  {mangled_name}__mutmut_orig.__name__ = '{mangled_name}'
@@ -447,6 +461,23 @@ class FuncContext:
447
461
  return False
448
462
 
449
463
 
464
+ def is_generator(node):
465
+ assert node.type == 'funcdef'
466
+
467
+ def _is_generator(n):
468
+ if n is not node and n.type in ('funcdef', 'classdef'):
469
+ return False
470
+
471
+ if n.type == 'keyword' and n.value == 'yield':
472
+ return True
473
+
474
+ for c in getattr(n, 'children', []):
475
+ if _is_generator(c):
476
+ return True
477
+ return False
478
+ return _is_generator(node)
479
+
480
+
450
481
  def yield_mutants_for_function(node, *, class_name=None, no_mutate_lines):
451
482
  assert node.type == 'funcdef'
452
483
 
@@ -481,7 +512,7 @@ def yield_mutants_for_function(node, *, class_name=None, no_mutate_lines):
481
512
  finally:
482
513
  context.stack.pop()
483
514
 
484
- trampoline = build_trampoline(node.name.value, context.mutants, class_name=class_name)
515
+ trampoline = build_trampoline(orig_name=node.name.value, mutants=context.mutants, class_name=class_name, is_generator=is_generator(node))
485
516
  if class_name is not None:
486
517
  trampoline = indent(trampoline, ' ')
487
518
  yield 'trampoline', trampoline, None, None
@@ -508,15 +539,38 @@ def yield_mutants_for_class_body(node, no_mutate_lines):
508
539
  yield 'filler', child_node.get_code(), None, None
509
540
 
510
541
 
542
+ def is_from_future_import_node(c):
543
+ if c.type == 'simple_stmt':
544
+ if c.children:
545
+ c2 = c.children[0]
546
+ if c2.type == 'import_from' and c2.children[1].type == 'name' and c2.children[1].value == '__future__':
547
+ return True
548
+ return False
549
+
550
+
551
+ def yield_future_imports(node):
552
+ for c in node.children:
553
+ if is_from_future_import_node(c):
554
+ yield 'filler', c.get_code(), None, None
555
+
556
+
511
557
  def yield_mutants_for_module(node, no_mutate_lines):
558
+ assert node.type == 'file_input'
559
+
560
+ # First yield `from __future__`, then the rest
561
+ yield from yield_future_imports(node)
562
+
512
563
  yield 'trampoline_impl', trampoline_impl, None, None
564
+ yield 'trampoline_impl', yield_from_trampoline_impl, None, None
513
565
  yield 'filler', '\n', None, None
514
- assert node.type == 'file_input'
515
566
  for child_node in node.children:
516
567
  if child_node.type == 'funcdef':
517
568
  yield from yield_mutants_for_function(child_node, no_mutate_lines=no_mutate_lines)
518
569
  elif child_node.type == 'classdef':
519
570
  yield from yield_mutants_for_class(child_node, no_mutate_lines=no_mutate_lines)
571
+ elif is_from_future_import_node(child_node):
572
+ # Don't yield `from __future__` after trampoline
573
+ pass
520
574
  else:
521
575
  yield 'filler', child_node.get_code(), None, None
522
576
 
@@ -604,28 +658,32 @@ def change_cwd(path):
604
658
  os.chdir(old_cwd)
605
659
 
606
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
+
607
669
  class ListAllTestsResult:
608
670
  def __init__(self, *, ids):
609
671
  assert isinstance(ids, set)
610
672
  self.ids = ids
611
673
 
612
- @staticmethod
613
- def old_tests():
614
- return {
615
- test_name
616
- for _, test_names in mutmut.tests_by_mangled_function_name.items()
617
- for test_name in test_names
618
- }
619
-
620
674
  def clear_out_obsolete_test_names(self):
675
+ count_before = len(mutmut.tests_by_mangled_function_name)
621
676
  mutmut.tests_by_mangled_function_name = {
622
- 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}
623
678
  for k, test_names in mutmut.tests_by_mangled_function_name.items()
624
679
  }
625
- 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()
626
684
 
627
685
  def new_tests(self):
628
- return self.ids - self.old_tests()
686
+ return self.ids - collected_test_names()
629
687
 
630
688
 
631
689
  class PytestRunner(TestRunner):
@@ -636,10 +694,7 @@ class PytestRunner(TestRunner):
636
694
  raise BadTestExecutionCommandsException(params)
637
695
  return exit_code
638
696
 
639
-
640
697
  def run_stats(self, *, tests):
641
- import pytest
642
-
643
698
  class StatsCollector:
644
699
  def pytest_runtest_teardown(self, item, nextitem):
645
700
  unused(nextitem)
@@ -719,6 +774,8 @@ def mangle_function_name(*, name, class_name):
719
774
  if class_name:
720
775
  assert CLASS_NAME_SEPARATOR not in class_name
721
776
  prefix = f'x{CLASS_NAME_SEPARATOR}{class_name}{CLASS_NAME_SEPARATOR}'
777
+ else:
778
+ prefix = 'x_'
722
779
  return f'{prefix}{name}'
723
780
 
724
781
 
@@ -729,10 +786,14 @@ def mangled_name_from_mutant_name(mutant_name):
729
786
 
730
787
  def orig_function_and_class_names_from_key(mutant_name):
731
788
  r = mangled_name_from_mutant_name(mutant_name)
789
+ _, _, r = r.rpartition('.')
732
790
  class_name = None
733
791
  if CLASS_NAME_SEPARATOR in r:
734
792
  class_name = r[r.index(CLASS_NAME_SEPARATOR) + 1: r.rindex(CLASS_NAME_SEPARATOR)]
735
793
  r = r[r.rindex(CLASS_NAME_SEPARATOR) + 1:]
794
+ else:
795
+ assert r.startswith('x_'), r
796
+ r = r[2:]
736
797
  return r, class_name
737
798
 
738
799
 
@@ -816,8 +877,7 @@ def run_forced_fail(runner):
816
877
  with CatchOutput(show_spinner=True, spinner_title='running forced fail test') as catcher:
817
878
  try:
818
879
  if runner.run_forced_fail() == 0:
819
- catcher.stop()
820
- print('\n'.join(catcher.strings))
880
+ catcher.dump_output()
821
881
  print("FAILED")
822
882
  os._exit(1)
823
883
  except MutmutProgrammaticFailException:
@@ -849,6 +909,8 @@ class CatchOutput:
849
909
  sys.stderr = sys.__stderr__
850
910
 
851
911
  def start(self):
912
+ if self.show_spinner:
913
+ print_status(self.spinner_title)
852
914
  sys.stdout = self.redirect
853
915
  sys.stderr = self.redirect
854
916
 
@@ -926,6 +988,8 @@ def run_stats_collection(runner, tests=None):
926
988
 
927
989
  os.environ['MUTANT_UNDER_TEST'] = 'stats'
928
990
  os.environ['PY_IGNORE_IMPORTMISMATCH'] = '1'
991
+ start_cpu_time = process_time()
992
+
929
993
  with CatchOutput(show_spinner=True, spinner_title='running stats') as output_catcher:
930
994
  collect_stats_exit_code = runner.run_stats(tests=tests)
931
995
  if collect_stats_exit_code != 0:
@@ -934,10 +998,12 @@ def run_stats_collection(runner, tests=None):
934
998
  exit(1)
935
999
 
936
1000
  print(' done')
1001
+ if not tests: # again, meaning all
1002
+ mutmut.stats_time = process_time() - start_cpu_time
937
1003
 
938
- if not mutmut.tests_by_mangled_function_name:
1004
+ if not collected_test_names():
939
1005
  print('failed to collect stats, no active tests found')
940
- return
1006
+ exit(1)
941
1007
 
942
1008
  save_stats()
943
1009
 
@@ -975,6 +1041,7 @@ def load_stats():
975
1041
  for k, v in data.pop('tests_by_mangled_function_name').items():
976
1042
  mutmut.tests_by_mangled_function_name[k] |= set(v)
977
1043
  mutmut.duration_by_test = data.pop('duration_by_test')
1044
+ mutmut.stats_time = data.pop('stats_time')
978
1045
  assert not data, data
979
1046
  did_load = True
980
1047
  except (FileNotFoundError, JSONDecodeError):
@@ -987,6 +1054,7 @@ def save_stats():
987
1054
  json.dump(dict(
988
1055
  tests_by_mangled_function_name={k: list(v) for k, v in mutmut.tests_by_mangled_function_name.items()},
989
1056
  duration_by_test=mutmut.duration_by_test,
1057
+ stats_time=mutmut.stats_time,
990
1058
  ), f, indent=4)
991
1059
 
992
1060
 
@@ -1083,7 +1151,14 @@ def run(mutant_names, *, max_children):
1083
1151
  time = datetime.now() - start
1084
1152
  print(f' done in {round(time.total_seconds()*1000)}ms', )
1085
1153
 
1086
- sys.path.insert(0, os.path.abspath('mutants'))
1154
+ src_path = (Path('mutants') / 'src')
1155
+ source_path = (Path('mutants') / 'source')
1156
+ if src_path.exists():
1157
+ sys.path.insert(0, str(src_path.absolute()))
1158
+ elif source_path.exists():
1159
+ sys.path.insert(0, str(source_path.absolute))
1160
+ else:
1161
+ sys.path.insert(0, os.path.abspath('mutants'))
1087
1162
 
1088
1163
  # TODO: config/option for runner
1089
1164
  # runner = HammettRunner()
@@ -1160,6 +1235,9 @@ def run(mutant_names, *, max_children):
1160
1235
  if not tests:
1161
1236
  os._exit(33)
1162
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
+
1163
1241
  with CatchOutput():
1164
1242
  result = runner.run_tests(mutant_name=mutant_name, tests=tests)
1165
1243
 
@@ -1367,7 +1445,9 @@ def browse():
1367
1445
  from textual.containers import Container
1368
1446
  from textual.widgets import Footer
1369
1447
  from textual.widgets import DataTable
1370
- from textual.widgets import TextArea
1448
+ from textual.widgets import Static
1449
+ from textual.widget import Widget
1450
+ from rich.syntax import Syntax
1371
1451
 
1372
1452
  class ResultBrowser(App):
1373
1453
  loading_id = None
@@ -1394,7 +1474,8 @@ def browse():
1394
1474
  with Container(classes='container'):
1395
1475
  yield DataTable(id='files')
1396
1476
  yield DataTable(id='mutants')
1397
- yield TextArea(id='diff_view')
1477
+ with Widget(id="diff_view_widget"):
1478
+ yield Static(id='diff_view')
1398
1479
  yield Footer()
1399
1480
 
1400
1481
  def on_mount(self):
@@ -1453,9 +1534,9 @@ def browse():
1453
1534
  assert event.data_table.id == 'mutants'
1454
1535
  diff_view = self.query_one('#diff_view')
1455
1536
  if event.row_key.value is None:
1456
- diff_view.text = ''
1537
+ diff_view.update('')
1457
1538
  else:
1458
- diff_view.text = '<loading...>'
1539
+ diff_view.update('<loading...>')
1459
1540
  self.loading_id = event.row_key.value
1460
1541
 
1461
1542
  def load_thread():
@@ -1463,9 +1544,9 @@ def browse():
1463
1544
  try:
1464
1545
  d = get_diff_for_mutant(event.row_key.value)
1465
1546
  if event.row_key.value == self.loading_id:
1466
- diff_view.text = d
1547
+ diff_view.update(Syntax(d, "diff"))
1467
1548
  except Exception as e:
1468
- diff_view.text = f'<{e}>'
1549
+ diff_view.update(f"<{type(e)} {e}>")
1469
1550
 
1470
1551
  t = Thread(target=load_thread)
1471
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.0.5
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
@@ -19,4 +19,8 @@ mutmut.egg-info/dependency_links.txt
19
19
  mutmut.egg-info/entry_points.txt
20
20
  mutmut.egg-info/not-zip-safe
21
21
  mutmut.egg-info/requires.txt
22
- mutmut.egg-info/top_level.txt
22
+ mutmut.egg-info/top_level.txt
23
+ mutmut/__pycache__/__init__.cpython-311.pyc
24
+ mutmut/__pycache__/__init__.cpython-312.pyc
25
+ mutmut/__pycache__/__main__.cpython-311.pyc
26
+ mutmut/__pycache__/__main__.cpython-312.pyc
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes