mutmut 3.3.0__tar.gz → 3.3.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.
Files changed (37) hide show
  1. {mutmut-3.3.0 → mutmut-3.3.1}/HISTORY.rst +16 -0
  2. {mutmut-3.3.0 → mutmut-3.3.1}/PKG-INFO +18 -1
  3. {mutmut-3.3.0 → mutmut-3.3.1}/README.rst +17 -0
  4. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/.DS_Store +0 -0
  5. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__init__.py +1 -1
  6. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__main__.py +109 -36
  7. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/file_mutation.py +3 -33
  8. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/node_mutation.py +39 -12
  9. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/trampoline_templates.py +5 -11
  10. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/PKG-INFO +18 -1
  11. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/SOURCES.txt +1 -0
  12. mutmut-3.3.1/tests/test_generation_error_handling.py +35 -0
  13. {mutmut-3.3.0 → mutmut-3.3.1}/tests/test_mutation.py +68 -55
  14. {mutmut-3.3.0 → mutmut-3.3.1}/tests/test_mutmut3.py +3 -6
  15. {mutmut-3.3.0 → mutmut-3.3.1}/AUTHORS.rst +0 -0
  16. {mutmut-3.3.0 → mutmut-3.3.1}/LICENSE +0 -0
  17. {mutmut-3.3.0 → mutmut-3.3.1}/MANIFEST.in +0 -0
  18. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__init__.cpython-311.pyc +0 -0
  19. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__init__.cpython-312.pyc +0 -0
  20. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__main__.cpython-311.pyc +0 -0
  21. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/__main__.cpython-312.pyc +0 -0
  22. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/file_mutation.cpython-312.pyc +0 -0
  23. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/node_mutation.cpython-312.pyc +0 -0
  24. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/__pycache__/trampoline_templates.cpython-312.pyc +0 -0
  25. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut/result_browser_layout.tcss +0 -0
  26. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/dependency_links.txt +0 -0
  27. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/entry_points.txt +0 -0
  28. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/not-zip-safe +0 -0
  29. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/requires.txt +0 -0
  30. {mutmut-3.3.0 → mutmut-3.3.1}/mutmut.egg-info/top_level.txt +0 -0
  31. {mutmut-3.3.0 → mutmut-3.3.1}/requirements.txt +0 -0
  32. {mutmut-3.3.0 → mutmut-3.3.1}/setup.cfg +0 -0
  33. {mutmut-3.3.0 → mutmut-3.3.1}/setup.py +0 -0
  34. {mutmut-3.3.0 → mutmut-3.3.1}/tests/__init__.py +0 -0
  35. {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/__init__.py +0 -0
  36. {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/test_cli_version.py +0 -0
  37. {mutmut-3.3.0 → mutmut-3.3.1}/tests/e2e/test_e2e_result_snapshots.py +0 -0
@@ -1,6 +1,22 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 3.3.1
5
+ ~~~~~
6
+
7
+ * Increased threshold for mutant timeouts
8
+
9
+ * Added `tests_dir` config. Accepts a single entry or a list of directories.
10
+
11
+ * Async generators fixes
12
+
13
+ * Fixed bad mutations for certain string escape sequences
14
+
15
+ * Performance fixes
16
+
17
+ * Various internal bug fixes
18
+
19
+
4
20
  3.3.0
5
21
  ~~~~~
6
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.3.0
3
+ Version: 3.3.1
4
4
  Summary: mutation testing for Python 3
5
5
  Home-page: https://github.com/boxed/mutmut
6
6
  Author: Anders Hovmöller
@@ -88,6 +88,9 @@ You can also write a mutant to disk from the `browse` interface, or via
88
88
  source code control and committed before you apply a mutant!
89
89
 
90
90
 
91
+ If during the installation you get an error for the `libcst` dependency mentioning the lack of a rust compiler on your system, it is because your architecture does not have a prebuilt binary for `libcst` and it requires both `rustc` and `cargo` from the [rust toolchain](https://www.rust-lang.org/tools/install) to be built. This is known for at least the `x86_64-darwin` architecture.
92
+
93
+
91
94
  Configuration
92
95
  -------------
93
96
 
@@ -173,6 +176,20 @@ You can exclude files from mutation in `setup.cfg`:
173
176
  *__tests.py
174
177
 
175
178
 
179
+ Enable debug output (increase verbosity)
180
+ ----------------------------------------
181
+
182
+ By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
183
+
184
+ If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
185
+ Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
186
+ to failing tests.
187
+
188
+ .. code-block::
189
+
190
+ debug=true
191
+
192
+
176
193
  Whitelisting
177
194
  ------------
178
195
 
@@ -64,6 +64,9 @@ You can also write a mutant to disk from the `browse` interface, or via
64
64
  source code control and committed before you apply a mutant!
65
65
 
66
66
 
67
+ If during the installation you get an error for the `libcst` dependency mentioning the lack of a rust compiler on your system, it is because your architecture does not have a prebuilt binary for `libcst` and it requires both `rustc` and `cargo` from the [rust toolchain](https://www.rust-lang.org/tools/install) to be built. This is known for at least the `x86_64-darwin` architecture.
68
+
69
+
67
70
  Configuration
68
71
  -------------
69
72
 
@@ -149,6 +152,20 @@ You can exclude files from mutation in `setup.cfg`:
149
152
  *__tests.py
150
153
 
151
154
 
155
+ Enable debug output (increase verbosity)
156
+ ----------------------------------------
157
+
158
+ By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
159
+
160
+ If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
161
+ Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
162
+ to failing tests.
163
+
164
+ .. code-block::
165
+
166
+ debug=true
167
+
168
+
152
169
  Whitelisting
153
170
  ------------
154
171
 
@@ -1,6 +1,6 @@
1
1
  from collections import defaultdict
2
2
 
3
- __version__ = '3.3.0'
3
+ __version__ = '3.3.1'
4
4
 
5
5
 
6
6
  duration_by_test = {}
@@ -4,7 +4,7 @@ import gc
4
4
  import inspect
5
5
  import itertools
6
6
  import json
7
- from multiprocessing import Pool, set_start_method
7
+ from multiprocessing import Pool, set_start_method, Lock
8
8
  import os
9
9
  import resource
10
10
  import shutil
@@ -47,7 +47,9 @@ from typing import (
47
47
  Dict,
48
48
  List,
49
49
  Union,
50
+ Optional,
50
51
  )
52
+ import warnings
51
53
 
52
54
  import click
53
55
  import libcst as cst
@@ -77,6 +79,7 @@ status_by_exit_code = {
77
79
  34: 'skipped',
78
80
  35: 'suspicious',
79
81
  36: 'timeout',
82
+ -24: 'timeout', # SIGXCPU
80
83
  24: 'timeout', # SIGXCPU
81
84
  152: 'timeout', # SIGXCPU
82
85
  255: 'timeout',
@@ -176,6 +179,13 @@ class BadTestExecutionCommandsException(Exception):
176
179
  super().__init__(msg)
177
180
 
178
181
 
182
+ class InvalidGeneratedSyntaxException(Exception):
183
+ def __init__(self, file: Union[Path, str]) -> None:
184
+ super().__init__(f'Mutmut generated invalid python syntax for {file}. '
185
+ 'If the original file has valid python syntax, please file an issue '
186
+ 'with a minimal reproducible example file.')
187
+
188
+
179
189
  def copy_src_dir():
180
190
  for path in mutmut.config.paths_to_mutate:
181
191
  output_path: Path = Path('mutants') / path
@@ -185,21 +195,33 @@ def copy_src_dir():
185
195
  output_path.parent.mkdir(exist_ok=True, parents=True)
186
196
  shutil.copyfile(path, output_path)
187
197
 
198
+ @dataclass
199
+ class FileMutationResult:
200
+ """Dataclass to transfer warnings and errors from child processes to the parent"""
201
+ warnings: list[Warning]
202
+ error: Optional[Exception] = None
188
203
 
189
204
  def create_mutants(max_children: int):
190
205
  with Pool(processes=max_children) as p:
191
- p.map(create_file_mutants, walk_source_files())
192
-
206
+ for result in p.imap_unordered(create_file_mutants, walk_source_files()):
207
+ for warning in result.warnings:
208
+ warnings.warn(warning)
209
+ if result.error:
210
+ raise result.error
193
211
 
194
- def create_file_mutants(path: Path):
195
- print(path)
196
- output_path = Path('mutants') / path
197
- makedirs(output_path.parent, exist_ok=True)
212
+ def create_file_mutants(path: Path) -> FileMutationResult:
213
+ try:
214
+ print(path)
215
+ output_path = Path('mutants') / path
216
+ makedirs(output_path.parent, exist_ok=True)
198
217
 
199
- if mutmut.config.should_ignore_for_mutation(path):
200
- shutil.copy(path, output_path)
201
- else:
202
- create_mutants_for_file(path, output_path)
218
+ if mutmut.config.should_ignore_for_mutation(path):
219
+ shutil.copy(path, output_path)
220
+ return FileMutationResult(warnings=[])
221
+ else:
222
+ return create_mutants_for_file(path, output_path)
223
+ except Exception as e:
224
+ return FileMutationResult(warnings=[], error=e)
203
225
 
204
226
 
205
227
  def copy_also_copy_files():
@@ -215,23 +237,30 @@ def copy_also_copy_files():
215
237
  else:
216
238
  shutil.copytree(path, destination, dirs_exist_ok=True)
217
239
 
218
-
219
- def create_mutants_for_file(filename, output_path):
240
+ def create_mutants_for_file(filename, output_path) -> FileMutationResult:
220
241
  input_stat = os.stat(filename)
242
+ warnings: list[Warning] = []
221
243
 
222
244
  with open(filename) as f:
223
245
  source = f.read()
224
246
 
225
247
  with open(output_path, 'w') as out:
226
- mutant_names, hash_by_function_name = write_all_mutants_to_file(out=out, source=source, filename=filename)
248
+ try:
249
+ mutant_names, hash_by_function_name = write_all_mutants_to_file(out=out, source=source, filename=filename)
250
+ except cst.ParserSyntaxError as e:
251
+ # if libcst cannot parse it, then copy the source without any mutations
252
+ warnings.append(SyntaxWarning(f'Unsupported syntax in {filename} ({str(e)}), skipping'))
253
+ out.write(source)
254
+ mutant_names, hash_by_function_name = [], {}
227
255
 
228
256
  # validate no syntax errors of mutants
229
257
  with open(output_path) as f:
230
258
  try:
231
259
  ast.parse(f.read())
232
260
  except (IndentationError, SyntaxError) as e:
233
- print(output_path, 'has invalid syntax: ', e)
234
- exit(1)
261
+ invalid_syntax_error = InvalidGeneratedSyntaxException(output_path)
262
+ invalid_syntax_error.__cause__ = e
263
+ return FileMutationResult(warnings=warnings, error=invalid_syntax_error)
235
264
 
236
265
  source_file_mutation_data = SourceFileMutationData(path=filename)
237
266
  module_name = strip_prefix(str(filename)[:-len(filename.suffix)].replace(os.sep, '.'), prefix='src.')
@@ -245,6 +274,7 @@ def create_mutants_for_file(filename, output_path):
245
274
  source_file_mutation_data.save()
246
275
 
247
276
  os.utime(output_path, (input_stat.st_atime, input_stat.st_mtime))
277
+ return FileMutationResult(warnings=warnings)
248
278
 
249
279
 
250
280
  def write_all_mutants_to_file(*, out, source, filename):
@@ -282,7 +312,8 @@ class SourceFileMutationData:
282
312
 
283
313
  def register_pid(self, *, pid, key, estimated_time_of_tests):
284
314
  self.key_by_pid[pid] = key
285
- self.start_time_by_pid[pid] = datetime.now()
315
+ with START_TIMES_BY_PID_LOCK:
316
+ self.start_time_by_pid[pid] = datetime.now()
286
317
  self.estimated_time_of_tests_by_pid[pid] = estimated_time_of_tests
287
318
 
288
319
  def register_result(self, *, pid, exit_code):
@@ -290,7 +321,8 @@ class SourceFileMutationData:
290
321
  self.exit_code_by_key[self.key_by_pid[pid]] = exit_code
291
322
  # TODO: maybe rate limit this? Saving on each result can slow down mutation testing a lot if the test run is fast.
292
323
  del self.key_by_pid[pid]
293
- del self.start_time_by_pid[pid]
324
+ with START_TIMES_BY_PID_LOCK:
325
+ del self.start_time_by_pid[pid]
294
326
  self.save()
295
327
 
296
328
  def stop_children(self):
@@ -397,16 +429,34 @@ class PytestRunner(TestRunner):
397
429
 
398
430
  stats_collector = StatsCollector()
399
431
 
432
+ pytest_args = ['-x', '-q']
433
+ if tests:
434
+ pytest_args += list(tests)
435
+ else:
436
+ tests_dir = mutmut.config.tests_dir
437
+ if tests_dir:
438
+ pytest_args += tests_dir
400
439
  with change_cwd('mutants'):
401
- return int(self.execute_pytest(['-x', '-q'] + list(tests), plugins=[stats_collector]))
440
+ return int(self.execute_pytest(pytest_args, plugins=[stats_collector]))
402
441
 
403
442
  def run_tests(self, *, mutant_name, tests):
443
+ pytest_args = ['-x', '-q']
444
+ if tests:
445
+ pytest_args += list(tests)
446
+ else:
447
+ tests_dir = mutmut.config.tests_dir
448
+ if tests_dir:
449
+ pytest_args += tests_dir
404
450
  with change_cwd('mutants'):
405
- return int(self.execute_pytest(['-x', '-q'] + list(tests)))
451
+ return int(self.execute_pytest(pytest_args))
406
452
 
407
453
  def run_forced_fail(self):
454
+ pytest_args = ['-x', '-q']
455
+ tests_dir = mutmut.config.tests_dir
456
+ if tests_dir:
457
+ pytest_args += tests_dir
408
458
  with change_cwd('mutants'):
409
- return int(self.execute_pytest(['-x', '-q']))
459
+ return int(self.execute_pytest(pytest_args))
410
460
 
411
461
  def list_all_tests(self):
412
462
  class TestsCollector:
@@ -415,8 +465,13 @@ class PytestRunner(TestRunner):
415
465
 
416
466
  collector = TestsCollector()
417
467
 
468
+ tests_dir = mutmut.config.tests_dir
469
+ pytest_args = ['-x', '-q', '--collect-only']
470
+ if tests_dir:
471
+ pytest_args += tests_dir
472
+
418
473
  with change_cwd('mutants'):
419
- exit_code = int(self.execute_pytest(['-x', '-q', '--collect-only'], plugins=[collector]))
474
+ exit_code = int(self.execute_pytest(pytest_args, plugins=[collector]))
420
475
  if exit_code != 0:
421
476
  raise CollectTestsFailedException()
422
477
 
@@ -620,6 +675,7 @@ class Config:
620
675
  max_stack_depth: int
621
676
  debug: bool
622
677
  paths_to_mutate: List[Path]
678
+ tests_dir: List[str] = None
623
679
 
624
680
  def should_ignore_for_mutation(self, path):
625
681
  if not str(path).endswith('.py'):
@@ -698,7 +754,8 @@ def load_config():
698
754
  paths_to_mutate=[
699
755
  Path(y)
700
756
  for y in s('paths_to_mutate', [])
701
- ] or guess_paths_to_mutate()
757
+ ] or guess_paths_to_mutate(),
758
+ tests_dir=s('tests_dir', []),
702
759
  )
703
760
 
704
761
 
@@ -860,6 +917,9 @@ def stop_all_children(mutants):
860
917
  for m, _, _ in mutants:
861
918
  m.stop_children()
862
919
 
920
+ # used to copy the global mutmut.config to subprocesses
921
+ set_start_method('fork')
922
+ START_TIMES_BY_PID_LOCK = Lock()
863
923
 
864
924
  def timeout_checker(mutants):
865
925
  def inner_timout_checker():
@@ -868,9 +928,12 @@ def timeout_checker(mutants):
868
928
 
869
929
  now = datetime.now()
870
930
  for m, mutant_name, result in mutants:
871
- for pid, start_time in m.start_time_by_pid.items():
931
+ # copy dict inside lock, so it is not modified by another process while we iterate it
932
+ with START_TIMES_BY_PID_LOCK:
933
+ start_times_by_pid = dict(m.start_time_by_pid)
934
+ for pid, start_time in start_times_by_pid.items():
872
935
  run_time = now - start_time
873
- if run_time.total_seconds() > (m.estimated_time_of_tests_by_mutant[mutant_name] + 1) * 4:
936
+ if run_time.total_seconds() > (m.estimated_time_of_tests_by_mutant[mutant_name] + 1) * 15:
874
937
  try:
875
938
  os.kill(pid, signal.SIGXCPU)
876
939
  except ProcessLookupError:
@@ -882,9 +945,6 @@ def timeout_checker(mutants):
882
945
  @click.option('--max-children', type=int)
883
946
  @click.argument('mutant_names', required=False, nargs=-1)
884
947
  def run(mutant_names, *, max_children):
885
- # used to copy the global mutmut.config to subprocesses
886
- set_start_method('fork')
887
-
888
948
  assert isinstance(mutant_names, (tuple, list)), mutant_names
889
949
  _run(mutant_names, max_children)
890
950
 
@@ -1007,8 +1067,9 @@ def _run(mutant_names: Union[tuple, list], max_children: Union[None, int]):
1007
1067
  os._exit(33)
1008
1068
 
1009
1069
  estimated_time_of_tests = m.estimated_time_of_tests_by_mutant[mutant_name]
1010
- cpu_time_limit = ceil((estimated_time_of_tests + 1) * 2 + process_time()) * 10
1011
- resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit))
1070
+ cpu_time_limit = ceil((estimated_time_of_tests + 1) * 30 + process_time())
1071
+ # signal SIGXCPU after <cpu_time_limit>. One second later signal SIGKILL if it is still running
1072
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit + 1))
1012
1073
 
1013
1074
  with CatchOutput():
1014
1075
  result = runner.run_tests(mutant_name=mutant_name, tests=tests)
@@ -1098,16 +1159,24 @@ def read_orig_module(path) -> cst.Module:
1098
1159
  return cst.parse_module(f.read())
1099
1160
 
1100
1161
 
1101
- def find_function(module: cst.Module, name: str) -> Union[cst.FunctionDef, None]:
1162
+ def find_top_level_function_or_method(module: cst.Module, name: str) -> Union[cst.FunctionDef, None]:
1102
1163
  name = name.split('.')[-1]
1103
- return next(iter(m.findall(module, m.FunctionDef(m.Name(name)))), None) # type: ignore
1164
+ for child in module.body:
1165
+ if isinstance(child, cst.FunctionDef) and child.name.value == name:
1166
+ return child
1167
+ if isinstance(child, cst.ClassDef) and isinstance(child.body, cst.IndentedBlock):
1168
+ for method in child.body.body:
1169
+ if isinstance(method, cst.FunctionDef) and method.name.value == name:
1170
+ return method
1171
+
1172
+ return None
1104
1173
 
1105
1174
 
1106
1175
  def read_original_function(module: cst.Module, mutant_name: str):
1107
1176
  orig_function_name, _ = orig_function_and_class_names_from_key(mutant_name)
1108
1177
  orig_name = mangled_name_from_mutant_name(mutant_name) + '__mutmut_orig'
1109
1178
 
1110
- result = find_function(module, orig_name)
1179
+ result = find_top_level_function_or_method(module, orig_name)
1111
1180
  if not result:
1112
1181
  raise FileNotFoundError(f'Could not find original function "{orig_function_name}"')
1113
1182
  return result.with_changes(name = cst.Name(orig_function_name))
@@ -1116,7 +1185,7 @@ def read_original_function(module: cst.Module, mutant_name: str):
1116
1185
  def read_mutant_function(module: cst.Module, mutant_name: str):
1117
1186
  orig_function_name, _ = orig_function_and_class_names_from_key(mutant_name)
1118
1187
 
1119
- result = find_function(module, mutant_name)
1188
+ result = find_top_level_function_or_method(module, mutant_name)
1120
1189
  if not result:
1121
1190
  raise FileNotFoundError(f'Could not find original function "{orig_function_name}"')
1122
1191
  return result.with_changes(name = cst.Name(orig_function_name))
@@ -1189,7 +1258,7 @@ def apply_mutant(mutant_name):
1189
1258
  mutant_function = read_mutant_function(mutants_module, mutant_name)
1190
1259
  mutant_function = mutant_function.with_changes(name=cst.Name(orig_function_name))
1191
1260
 
1192
- original_function = find_function(orig_module, orig_function_name)
1261
+ original_function = find_top_level_function_or_method(orig_module, orig_function_name)
1193
1262
  if not original_function:
1194
1263
  raise FileNotFoundError(f'Could not apply mutant {mutant_name}')
1195
1264
 
@@ -1263,6 +1332,7 @@ def browse(show_killed):
1263
1332
  def read_data(self):
1264
1333
  ensure_config_loaded()
1265
1334
  self.source_file_mutation_data_and_stat_by_path = {}
1335
+ self.path_by_name = {}
1266
1336
 
1267
1337
  for p in walk_source_files():
1268
1338
  if mutmut.config.should_ignore_for_mutation(p):
@@ -1272,6 +1342,8 @@ def browse(show_killed):
1272
1342
  stat = collect_stat(source_file_mutation_data)
1273
1343
 
1274
1344
  self.source_file_mutation_data_and_stat_by_path[str(p)] = source_file_mutation_data, stat
1345
+ for name in source_file_mutation_data.exit_code_by_key:
1346
+ self.path_by_name[name] = p
1275
1347
 
1276
1348
  def populate_files_table(self):
1277
1349
  # noinspection PyTypeChecker
@@ -1310,11 +1382,12 @@ def browse(show_killed):
1310
1382
  else:
1311
1383
  diff_view.update('<loading...>')
1312
1384
  self.loading_id = event.row_key.value
1385
+ path = self.path_by_name.get(event.row_key.value)
1313
1386
 
1314
1387
  def load_thread():
1315
1388
  ensure_config_loaded()
1316
1389
  try:
1317
- d = get_diff_for_mutant(event.row_key.value)
1390
+ d = get_diff_for_mutant(event.row_key.value, path=path)
1318
1391
  if event.row_key.value == self.loading_id:
1319
1392
  diff_view.update(Syntax(d, "diff"))
1320
1393
  except Exception as e:
@@ -8,7 +8,7 @@ import warnings
8
8
  import libcst as cst
9
9
  from libcst.metadata import PositionProvider, MetadataWrapper
10
10
  import libcst.matchers as m
11
- from mutmut.trampoline_templates import build_trampoline, mangle_function_name, trampoline_impl, yield_from_trampoline_impl
11
+ from mutmut.trampoline_templates import build_trampoline, mangle_function_name, trampoline_impl
12
12
  from mutmut.node_mutation import mutation_operators, OPERATORS_TYPE
13
13
 
14
14
  NEVER_MUTATE_FUNCTION_NAMES = { "__getattribute__", "__setattr__", "__new__" }
@@ -25,11 +25,7 @@ def mutate_file_contents(filename: str, code: str) -> tuple[str, Sequence[str]]:
25
25
  """Create mutations for `code` and merge them to a single mutated file with trampolines.
26
26
 
27
27
  :return: A tuple of (mutated code, list of mutant function names)"""
28
- try:
29
- module, mutations = create_mutations(code)
30
- except cst.ParserSyntaxError as e:
31
- warnings.warn(SyntaxWarning(f'Unsupported syntax in {filename} ({str(e)}), skipping'))
32
- return code, []
28
+ module, mutations = create_mutations(code)
33
29
 
34
30
  return combine_mutations_to_source(module, mutations)
35
31
 
@@ -165,8 +161,6 @@ MODULE_STATEMENT = Union[cst.SimpleStatementLine, cst.BaseCompoundStatement]
165
161
  # convert str trampoline implementations to CST nodes with some whitespace
166
162
  trampoline_impl_cst = list(cst.parse_module(trampoline_impl).body)
167
163
  trampoline_impl_cst[-1] = trampoline_impl_cst[-1].with_changes(leading_lines = [cst.EmptyLine(), cst.EmptyLine()])
168
- yield_from_trampoline_impl_cst = list(cst.parse_module(yield_from_trampoline_impl).body)
169
- yield_from_trampoline_impl_cst[-1] = yield_from_trampoline_impl_cst[-1].with_changes(leading_lines = [cst.EmptyLine(), cst.EmptyLine()])
170
164
 
171
165
 
172
166
  def combine_mutations_to_source(module: cst.Module, mutations: Sequence[Mutation]) -> tuple[str, Sequence[str]]:
@@ -185,7 +179,6 @@ def combine_mutations_to_source(module: cst.Module, mutations: Sequence[Mutation
185
179
 
186
180
  # trampoline functions
187
181
  result.extend(trampoline_impl_cst)
188
- result.extend(yield_from_trampoline_impl_cst)
189
182
 
190
183
  mutations_within_function = group_by_top_level_node(mutations)
191
184
 
@@ -234,7 +227,6 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable
234
227
 
235
228
  name = function.name.value
236
229
  mangled_name = mangle_function_name(name=name, class_name=class_name) + '__mutmut'
237
- _is_generator = is_generator(function)
238
230
 
239
231
  # copy of original function
240
232
  nodes.append(function.with_changes(name=cst.Name(mangled_name + '_orig')))
@@ -248,7 +240,7 @@ def function_trampoline_arrangement(function: cst.FunctionDef, mutants: Iterable
248
240
  nodes.append(mutated_method) # type: ignore
249
241
 
250
242
  # trampoline that forwards the calls
251
- trampoline = list(cst.parse_module(build_trampoline(orig_name=name, mutants=mutant_names, class_name=class_name, is_generator=_is_generator)).body)
243
+ trampoline = list(cst.parse_module(build_trampoline(orig_name=name, mutants=mutant_names, class_name=class_name)).body)
252
244
  trampoline[0] = trampoline[0].with_changes(leading_lines=[cst.EmptyLine()])
253
245
  nodes.extend(trampoline)
254
246
 
@@ -274,28 +266,6 @@ def group_by_top_level_node(mutations: Sequence[Mutation]) -> Mapping[cst.CSTNod
274
266
 
275
267
  return grouped
276
268
 
277
- def is_generator(function: cst.FunctionDef) -> bool:
278
- """Return True if the function has yield statement(s)."""
279
- visitor = IsGeneratorVisitor(function)
280
- function.visit(visitor)
281
- return visitor.is_generator
282
-
283
- class IsGeneratorVisitor(cst.CSTVisitor):
284
- """Check if a function is a generator.
285
- We do so by checking if any child is a Yield statement, but not looking into inner function definitions."""
286
- def __init__(self, original_function: cst.FunctionDef):
287
- self.is_generator = False
288
- self.original_function: cst.FunctionDef = original_function
289
-
290
- def visit_FunctionDef(self, node):
291
- # do not recurse into inner function definitions
292
- if self.original_function != node:
293
- return False
294
-
295
- def visit_Yield(self, node):
296
- self.is_generator = True
297
- return False
298
-
299
269
  def pragma_no_mutate_lines(source: str) -> set[int]:
300
270
  return {
301
271
  i + 1
@@ -1,5 +1,6 @@
1
1
  """This module contains the mutations for indidvidual nodes, e.g. replacing a != b with a == b."""
2
- from typing import Any, Union
2
+ import re
3
+ from typing import Any, Union, cast
3
4
  from collections.abc import Callable, Iterable, Sequence
4
5
  import libcst as cst
5
6
  import libcst.matchers as m
@@ -11,6 +12,9 @@ OPERATORS_TYPE = Sequence[
11
12
  ]
12
13
  ]
13
14
 
15
+ # pattern to match (nearly) all chars in a string that are not part of an escape sequence
16
+ NON_ESCAPE_SEQUENCE = re.compile(r"((?<!\\)[^\\]+)")
17
+
14
18
  def operator_number(
15
19
  node: cst.BaseNumber
16
20
  ) -> Iterable[cst.BaseNumber]:
@@ -39,9 +43,9 @@ def operator_string(
39
43
 
40
44
  supported_str_mutations: list[Callable[[str], str]] = [
41
45
  lambda x: "XX" + x + "XX",
42
- lambda x: x.lower(),
43
- lambda x: x.upper(),
44
- lambda x: x.capitalize(),
46
+ # do not modify escape sequences, as this could break python syntax
47
+ lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).lower(), x),
48
+ lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).upper(), x),
45
49
  ]
46
50
 
47
51
  for mut_func in supported_str_mutations:
@@ -96,7 +100,7 @@ def operator_arg_removal(
96
100
  yield node.with_changes(args=[*node.args[:i], *node.args[i + 1 :]])
97
101
 
98
102
 
99
- supported_str_methods_swap = [
103
+ supported_symmetric_str_methods_swap = [
100
104
  ("lower", "upper"),
101
105
  ("upper", "lower"),
102
106
  ("lstrip", "rstrip"),
@@ -107,24 +111,42 @@ supported_str_methods_swap = [
107
111
  ("rjust", "ljust"),
108
112
  ("index", "rindex"),
109
113
  ("rindex", "index"),
110
- ("split", "rsplit"),
111
- ("rsplit", "split"),
112
114
  ("removeprefix", "removesuffix"),
113
115
  ("removesuffix", "removeprefix"),
114
116
  ("partition", "rpartition"),
115
117
  ("rpartition", "partition")
116
- ]
118
+ ]
119
+
120
+ supported_unsymmetrical_str_methods_swap = [
121
+ ("split", "rsplit"),
122
+ ("rsplit", "split")
123
+ ]
117
124
 
118
- def operator_string_methods_swap(
125
+ def operator_symmetric_string_methods_swap(
119
126
  node: cst.Call
120
127
  ) -> Iterable[cst.Call]:
121
128
  """try to swap string method to opposite e.g. a.lower() -> a.upper()"""
122
129
 
123
- for old_call, new_call in supported_str_methods_swap:
130
+ for old_call, new_call in supported_symmetric_str_methods_swap:
124
131
  if m.matches(node.func, m.Attribute(value=m.DoNotCare(), attr=m.Name(value=old_call))):
125
132
  func_name = cst.ensure_type(node.func, cst.Attribute).attr
126
133
  yield node.with_deep_changes(func_name, value=new_call)
127
134
 
135
+ def operator_unsymmetrical_string_methods_swap(
136
+ node: cst.Call
137
+ ) -> Iterable[cst.Call]:
138
+ """Try to handle specific mutations of string, which useful only in specific args combination."""
139
+ for old_call, new_call in supported_unsymmetrical_str_methods_swap:
140
+ if m.matches(node.func, m.Attribute(attr=m.Name(value=old_call))):
141
+ if old_call in {"split", "rsplit"}:
142
+ # The logic of this "if" operator described here:
143
+ # https://github.com/boxed/mutmut/pull/394#issuecomment-2977890188
144
+ key_args: set[str] = {a.keyword.value for a in node.args if a.keyword} # sep or maxsplit or nothing
145
+ if len(node.args) == 2 or "maxsplit" in key_args:
146
+ func_name = cst.ensure_type(node.func, cst.Attribute).attr
147
+ yield node.with_deep_changes(func_name, value=new_call)
148
+
149
+
128
150
 
129
151
  def operator_remove_unary_ops(
130
152
  node: cst.UnaryOperation
@@ -197,7 +219,11 @@ _operator_mapping: dict[type[cst.CSTNode], type[cst.CSTNode]] = {
197
219
  def operator_swap_op(
198
220
  node: cst.CSTNode
199
221
  ) -> Iterable[cst.CSTNode]:
200
- yield from _simple_mutation_mapping(node, _operator_mapping)
222
+ if m.matches(node, m.BinaryOperation() | m.UnaryOperation() | m.BooleanOperation() | m.ComparisonTarget() | m.AugAssign()):
223
+ typed_node = cast(Union[cst.BinaryOperation, cst.UnaryOperation, cst.BooleanOperation, cst.ComparisonTarget, cst.AugAssign], node)
224
+ operator = typed_node.operator
225
+ for new_operator in _simple_mutation_mapping(operator, _operator_mapping):
226
+ yield node.with_changes(operator=new_operator)
201
227
 
202
228
 
203
229
  def operator_augmented_assignment(
@@ -238,7 +264,8 @@ mutation_operators: OPERATORS_TYPE = [
238
264
  (cst.UnaryOperation, operator_remove_unary_ops),
239
265
  (cst.Call, operator_dict_arguments),
240
266
  (cst.Call, operator_arg_removal),
241
- (cst.Call, operator_string_methods_swap),
267
+ (cst.Call, operator_symmetric_string_methods_swap),
268
+ (cst.Call, operator_unsymmetrical_string_methods_swap),
242
269
  (cst.Lambda, operator_lambda),
243
270
  (cst.CSTNode, operator_keywords),
244
271
  (cst.CSTNode, operator_swap_op),
@@ -1,6 +1,6 @@
1
1
  CLASS_NAME_SEPARATOR = 'ǁ'
2
2
 
3
- def build_trampoline(*, orig_name, mutants, class_name, is_generator):
3
+ def build_trampoline(*, orig_name, mutants, class_name):
4
4
  mangled_name = mangle_function_name(name=orig_name, class_name=class_name)
5
5
 
6
6
  mutants_dict = f'{mangled_name}__mutmut_mutants : ClassVar[MutantDict] = {{\n' + ', \n '.join(f'{repr(m)}: {m}' for m in mutants) + '\n}'
@@ -12,18 +12,13 @@ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
12
12
  access_suffix = '")'
13
13
  self_arg = ', self'
14
14
 
15
- if is_generator:
16
- yield_statement = 'yield from ' # note the space at the end!
17
- trampoline_name = '_mutmut_yield_from_trampoline'
18
- else:
19
- yield_statement = ''
20
- trampoline_name = '_mutmut_trampoline'
15
+ trampoline_name = '_mutmut_trampoline'
21
16
 
22
17
  return f"""
23
18
  {mutants_dict}
24
19
 
25
20
  def {orig_name}({'self, ' if class_name is not None else ''}*args, **kwargs):
26
- result = {yield_statement}{trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, args, kwargs{self_arg})
21
+ result = {trampoline_name}({access_prefix}{mangled_name}__mutmut_orig{access_suffix}, {access_prefix}{mangled_name}__mutmut_mutants{access_suffix}, args, kwargs{self_arg})
27
22
  return result
28
23
 
29
24
  {orig_name}.__signature__ = _mutmut_signature({mangled_name}__mutmut_orig)
@@ -62,11 +57,11 @@ def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None):
62
57
  from mutmut.__main__ import record_trampoline_hit
63
58
  record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
64
59
  result = orig(*call_args, **call_kwargs)
65
- return result # for the yield case
60
+ return result
66
61
  prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
67
62
  if not mutant_under_test.startswith(prefix):
68
63
  result = orig(*call_args, **call_kwargs)
69
- return result # for the yield case
64
+ return result
70
65
  mutant_name = mutant_under_test.rpartition('.')[-1]
71
66
  if self_arg:
72
67
  # call to a class method where self is not bound
@@ -76,4 +71,3 @@ def _mutmut_trampoline(orig, mutants, call_args, call_kwargs, self_arg = None):
76
71
  return result
77
72
 
78
73
  """
79
- yield_from_trampoline_impl = trampoline_impl.replace('result = ', 'result = yield from ').replace('_mutmut_trampoline', '_mutmut_yield_from_trampoline')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mutmut
3
- Version: 3.3.0
3
+ Version: 3.3.1
4
4
  Summary: mutation testing for Python 3
5
5
  Home-page: https://github.com/boxed/mutmut
6
6
  Author: Anders Hovmöller
@@ -88,6 +88,9 @@ You can also write a mutant to disk from the `browse` interface, or via
88
88
  source code control and committed before you apply a mutant!
89
89
 
90
90
 
91
+ If during the installation you get an error for the `libcst` dependency mentioning the lack of a rust compiler on your system, it is because your architecture does not have a prebuilt binary for `libcst` and it requires both `rustc` and `cargo` from the [rust toolchain](https://www.rust-lang.org/tools/install) to be built. This is known for at least the `x86_64-darwin` architecture.
92
+
93
+
91
94
  Configuration
92
95
  -------------
93
96
 
@@ -173,6 +176,20 @@ You can exclude files from mutation in `setup.cfg`:
173
176
  *__tests.py
174
177
 
175
178
 
179
+ Enable debug output (increase verbosity)
180
+ ----------------------------------------
181
+
182
+ By default, mutmut "swallows" all the test output etc. so that you get a nice clean output.
183
+
184
+ If you want to see all the detail to aid with debugging, you can set `debug` to `true` in your configuration.
185
+ Note that not all displayed errors are necessarily bad. In particular test runs of the mutated code will lead
186
+ to failing tests.
187
+
188
+ .. code-block::
189
+
190
+ debug=true
191
+
192
+
176
193
  Whitelisting
177
194
  ------------
178
195
 
@@ -13,6 +13,7 @@ setup.py
13
13
  ./mutmut/result_browser_layout.tcss
14
14
  ./mutmut/trampoline_templates.py
15
15
  ./tests/__init__.py
16
+ ./tests/test_generation_error_handling.py
16
17
  ./tests/test_mutation.py
17
18
  ./tests/test_mutmut3.py
18
19
  ./tests/e2e/__init__.py
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+ from mutmut.__main__ import create_mutants, Config, InvalidGeneratedSyntaxException
3
+ import mutmut.__main__
4
+ import mutmut
5
+ import pytest
6
+ import os
7
+
8
+ source_dir = Path(__file__).parent / 'data' / 'test_generation'
9
+ source_dir = source_dir.relative_to(os.getcwd())
10
+
11
+ class MockConfig:
12
+ def should_ignore_for_mutation(self, path: Path) -> bool:
13
+ return False
14
+
15
+ def test_mutant_generation_raises_exception_on_invalid_syntax(monkeypatch):
16
+ mutmut._reset_globals()
17
+ mutmut.config = MockConfig()
18
+
19
+ source_files = [
20
+ source_dir / "valid_syntax_1.py",
21
+ source_dir / "valid_syntax_2.py",
22
+ source_dir / "valid_syntax_3.py",
23
+ source_dir / "valid_syntax_4.py",
24
+ source_dir / "invalid_syntax.py",
25
+ ]
26
+ monkeypatch.setattr(mutmut.__main__, "walk_source_files", lambda: source_files)
27
+ monkeypatch.setattr("mutmut.config.should_ignore_for_mutation", lambda _path: False)
28
+
29
+ # should raise an exception, because we copy the invalid_syntax.py file and then verify
30
+ # if it is valid syntax
31
+ with pytest.raises(InvalidGeneratedSyntaxException) as excinfo:
32
+ # should raise a warning, because libcst is not able to parse invalid_syntax.py
33
+ with pytest.warns(SyntaxWarning):
34
+ create_mutants(max_children=2)
35
+ assert 'invalid_syntax.py' in str(excinfo.value)
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from unittest.mock import Mock, patch
3
3
  import pytest
4
- from libcst import parse_statement
4
+ import libcst as cst
5
5
 
6
6
  import mutmut
7
7
  from mutmut.__main__ import (
@@ -13,8 +13,8 @@ from mutmut.__main__ import (
13
13
  MutmutProgrammaticFailException,
14
14
  CatchOutput,
15
15
  )
16
- from mutmut.trampoline_templates import trampoline_impl, yield_from_trampoline_impl, mangle_function_name
17
- from mutmut.file_mutation import create_mutations, mutate_file_contents, is_generator
16
+ from mutmut.trampoline_templates import trampoline_impl, mangle_function_name
17
+ from mutmut.file_mutation import create_mutations, mutate_file_contents
18
18
 
19
19
  def mutants_for_source(source: str) -> list[str]:
20
20
  module, mutated_nodes = create_mutations(source)
@@ -62,8 +62,47 @@ def mutated_module(source: str) -> str:
62
62
  ]),
63
63
  ('a.index("+")', ['a.rindex("+")', 'a.index("XX+XX")', 'a.index(None)']),
64
64
  ('a.rindex("+")', ['a.index("+")', 'a.rindex("XX+XX")', 'a.rindex(None)']),
65
- ('a.split()', 'a.rsplit()'),
66
- ('a.rsplit()', 'a.split()'),
65
+ ('a.split()', []),
66
+ ('a.rsplit()', []),
67
+ ('a.split(" ")', ['a.split("XX XX")', 'a.split(None)']),
68
+ ('a.rsplit(" ")', ['a.rsplit("XX XX")', 'a.rsplit(None)']),
69
+ ('a.split(sep="")', ['a.split(sep="XXXX")', 'a.split(sep=None)']),
70
+ ('a.rsplit(sep="")', ['a.rsplit(sep="XXXX")', 'a.rsplit(sep=None)']),
71
+ ('a.split(maxsplit=-1)', [
72
+ 'a.rsplit(maxsplit=-1)', 'a.split(maxsplit=+1)', 'a.split(maxsplit=-2)', 'a.split(maxsplit=None)'
73
+ ]),
74
+ ('a.rsplit(maxsplit=-1)', [
75
+ 'a.split(maxsplit=-1)', 'a.rsplit(maxsplit=+1)', 'a.rsplit(maxsplit=-2)', 'a.rsplit(maxsplit=None)'
76
+ ]),
77
+ ('a.split(" ", maxsplit=-1)', [
78
+ 'a.split(" ", )', 'a.split(" ", maxsplit=+1)', 'a.split(" ", maxsplit=-2)',
79
+ 'a.split(" ", maxsplit=None)', 'a.split("XX XX", maxsplit=-1)', 'a.split(None, maxsplit=-1)',
80
+ 'a.split(maxsplit=-1)', 'a.rsplit(" ", maxsplit=-1)'
81
+ ]),
82
+ ('a.rsplit(" ", maxsplit=-1)', [
83
+ 'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=+1)', 'a.rsplit(" ", maxsplit=-2)',
84
+ 'a.rsplit(" ", maxsplit=None)', 'a.rsplit("XX XX", maxsplit=-1)', 'a.rsplit(None, maxsplit=-1)',
85
+ 'a.rsplit(maxsplit=-1)', 'a.split(" ", maxsplit=-1)'
86
+ ]),
87
+ ('a.split(maxsplit=1)', ['a.split(maxsplit=2)', 'a.split(maxsplit=None)', 'a.rsplit(maxsplit=1)']),
88
+ ('a.rsplit(maxsplit=1)', ['a.rsplit(maxsplit=2)', 'a.rsplit(maxsplit=None)', 'a.split(maxsplit=1)']),
89
+ ('a.split(" ", 1)', [
90
+ 'a.rsplit(" ", 1)', 'a.split(" ", )', 'a.split(" ", 2)', 'a.split(" ", None)',
91
+ 'a.split("XX XX", 1)', 'a.split(1)', 'a.split(None, 1)'
92
+ ]),
93
+ ('a.rsplit(" ", 1)', [
94
+ 'a.rsplit(" ", )', 'a.rsplit(" ", 2)', 'a.rsplit(" ", None)', 'a.rsplit("XX XX", 1)',
95
+ 'a.rsplit(1)', 'a.rsplit(None, 1)', 'a.split(" ", 1)'
96
+ ]),
97
+ ('a.split(" ", maxsplit=1)', [
98
+ 'a.rsplit(" ", maxsplit=1)', 'a.split(" ", )', 'a.split(" ", maxsplit=2)', 'a.split(" ", maxsplit=None)',
99
+ 'a.split("XX XX", maxsplit=1)', 'a.split(None, maxsplit=1)', 'a.split(maxsplit=1)'
100
+ ]),
101
+ ('a.rsplit(" ", maxsplit=1)', [
102
+ 'a.rsplit(" ", )', 'a.rsplit(" ", maxsplit=2)', 'a.rsplit(" ", maxsplit=None)',
103
+ 'a.rsplit("XX XX", maxsplit=1)', 'a.rsplit(None, maxsplit=1)', 'a.rsplit(maxsplit=1)',
104
+ 'a.split(" ", maxsplit=1)'
105
+ ]),
67
106
  ('a.removeprefix("+")', ['a.removesuffix("+")', 'a.removeprefix("XX+XX")', 'a.removeprefix(None)']),
68
107
  ('a.removesuffix("+")', ['a.removeprefix("+")', 'a.removesuffix("XX+XX")', 'a.removesuffix(None)']),
69
108
  ('a.partition("++")', ['a.rpartition("++")', 'a.partition("XX++XX")', 'a.partition(None)']),
@@ -86,7 +125,9 @@ def mutated_module(source: str) -> str:
86
125
  ('x: list[A | None]', []),
87
126
  ('a: Optional[int] = None', 'a: Optional[int] = ""'),
88
127
  ('a: int = 1', ['a: int = 2', 'a: int = None']),
89
- ('a: str = "FoO"', ['a: str = "XXFoOXX"', 'a: str = "foo"', 'a: str = "FOO"', 'a: str = "Foo"', 'a: str = None']),
128
+ ('a: str = "FoO"', ['a: str = "XXFoOXX"', 'a: str = "foo"', 'a: str = "FOO"', 'a: str = None']),
129
+ (r'a: str = "Fo\t"', [r'a: str = "XXFo\tXX"', r'a: str = "FO\t"', r'a: str = "fo\t"', 'a: str = None']),
130
+ (r'a: str = "Fo\N{ghost} \U11223344"', [r'a: str = "XXFo\N{ghost} \U11223344XX"', r'a: str = "FO\N{GHOST} \U11223344"', r'a: str = "fo\N{ghost} \U11223344"', 'a: str = None']),
90
131
  ('lambda: 0', ['lambda: 1', 'lambda: None']),
91
132
  ("1 in (1, 2)", ['2 in (1, 2)', '1 not in (1, 2)', '1 in (2, 2)', '1 in (1, 3)']),
92
133
  ('1+1', ['2+1', '1 - 1', '1+2']),
@@ -109,9 +150,9 @@ def mutated_module(source: str) -> str:
109
150
  ('1e-3', '1.001'),
110
151
  ('True', 'False'),
111
152
  ('False', 'True'),
112
- ('"FoO"', ['"XXFoOXX"', '"foo"', '"FOO"', '"Foo"']),
113
- ("'FoO'", ["'XXFoOXX'", "'foo'", "'FOO'", "'Foo'"]),
114
- ("u'FoO'", ["u'XXFoOXX'", "u'foo'", "u'FOO'", "u'Foo'"]),
153
+ ('"FoO"', ['"XXFoOXX"', '"foo"', '"FOO"']),
154
+ ("'FoO'", ["'XXFoOXX'", "'foo'", "'FOO'"]),
155
+ ("u'FoO'", ["u'XXFoOXX'", "u'foo'", "u'FOO'"]),
115
156
  ("10", "11"),
116
157
  ("10.", "11.0"),
117
158
  ("0o10", "9"),
@@ -244,6 +285,19 @@ match x:
244
285
 
245
286
  assert sorted(mutants) == sorted(expected)
246
287
 
288
+ def test_mach_case_does_not_mutate_bitor():
289
+ source = """
290
+ def concat():
291
+ match x:
292
+ case A() | B():
293
+ pass
294
+ """
295
+
296
+ mutants = mutants_for_source(source)
297
+
298
+ assert sorted(mutants) == []
299
+
300
+
247
301
  def test_basic_class():
248
302
  source = """
249
303
  class Foo:
@@ -269,8 +323,8 @@ def test_function_with_annotation():
269
323
  print(mutated_code)
270
324
 
271
325
  expected_defs = [
272
- 'def x_capitalize__mutmut_1(s : str):\n return s[1].title() + s[1:] if s else s',
273
- 'def x_capitalize__mutmut_2(s : str):\n return s[0].title() - s[1:] if s else s',
326
+ 'def x_capitalize__mutmut_1(s : str):\n return s[0].title() - s[1:] if s else s',
327
+ 'def x_capitalize__mutmut_2(s : str):\n return s[1].title() + s[1:] if s else s',
274
328
  'def x_capitalize__mutmut_3(s : str):\n return s[0].title() + s[2:] if s else s',
275
329
  ]
276
330
 
@@ -315,13 +369,10 @@ def test_mutate_dict():
315
369
  assert sorted(mutants) == sorted(expected)
316
370
 
317
371
 
318
- def test_syntax_warning():
319
- with pytest.warns(SyntaxWarning) as record:
372
+ def test_syntax_error():
373
+ with pytest.raises(cst.ParserSyntaxError):
320
374
  mutate_file_contents('some_file.py', ':!')
321
375
 
322
- assert len(record) == 1
323
- assert 'some_file.py' in record[0].message.args[0] # type: ignore
324
-
325
376
 
326
377
  def test_bug_github_issue_18():
327
378
  source = """
@@ -460,43 +511,6 @@ def foo():
460
511
  assert mutated_source.count('from __future__') == 1
461
512
 
462
513
 
463
- def test_preserve_generators():
464
- source = '''
465
- def foo():
466
- yield 1
467
- '''.strip()
468
- mutated_source = mutated_module(source)
469
- assert 'yield from _mutmut_yield_from_trampoline' in mutated_source
470
-
471
-
472
- def test_is_generator():
473
- source = '''
474
- def foo():
475
- yield 1
476
- '''.strip()
477
- assert is_generator(parse_statement(source)) # type: ignore
478
-
479
- source = '''
480
- def foo():
481
- yield from bar()
482
- '''.strip()
483
- assert is_generator(parse_statement(source)) # type: ignore
484
-
485
- source = '''
486
- def foo():
487
- return 1
488
- '''.strip()
489
- assert not is_generator(parse_statement(source)) # type: ignore
490
-
491
- source = '''
492
- def foo():
493
- def bar():
494
- yield 2
495
- return 1
496
- '''.strip()
497
- assert not is_generator(parse_statement(source)) # type: ignore
498
-
499
-
500
514
  # Negate the effects of CatchOutput because it does not play nicely with capfd in GitHub Actions
501
515
  @patch.object(CatchOutput, 'dump_output')
502
516
  @patch.object(CatchOutput, 'stop')
@@ -624,7 +638,6 @@ import lib
624
638
 
625
639
  lib.foo()
626
640
  {trampoline_impl.strip()}
627
- {yield_from_trampoline_impl.strip()}
628
641
 
629
642
  def x_foo__mutmut_orig(a, b):
630
643
  return a > b
@@ -654,7 +667,7 @@ x_bar__mutmut_mutants : ClassVar[MutantDict] = {{
654
667
  }}
655
668
 
656
669
  def bar(*args, **kwargs):
657
- result = yield from _mutmut_yield_from_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs)
670
+ result = _mutmut_trampoline(x_bar__mutmut_orig, x_bar__mutmut_mutants, args, kwargs)
658
671
  return result
659
672
 
660
673
  bar.__signature__ = _mutmut_signature(x_bar__mutmut_orig)
@@ -1,7 +1,4 @@
1
- from mutmut.trampoline_templates import (
2
- trampoline_impl,
3
- yield_from_trampoline_impl,
4
- )
1
+ from mutmut.trampoline_templates import trampoline_impl
5
2
  from mutmut.file_mutation import mutate_file_contents
6
3
 
7
4
  def mutated_module(source: str) -> str:
@@ -16,7 +13,7 @@ a + 1
16
13
  def foo(a, b, c):
17
14
  return a + b * c
18
15
  """
19
- trampolines = trampoline_impl.removesuffix('\n\n') + yield_from_trampoline_impl.removesuffix('\n\n')
16
+ trampolines = trampoline_impl.removesuffix('\n\n')
20
17
 
21
18
  expected = f"""
22
19
  a + 1{trampolines}
@@ -54,7 +51,7 @@ def foo(a: List[int]) -> int:
54
51
  return 1
55
52
  """
56
53
 
57
- expected = trampoline_impl.removesuffix('\n\n') + yield_from_trampoline_impl.removesuffix('\n\n') + """
54
+ expected = trampoline_impl.removesuffix('\n\n') + """
58
55
  def x_foo__mutmut_orig(a: List[int]) -> int:
59
56
  return 1
60
57
  def x_foo__mutmut_1(a: List[int]) -> int:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes