Skip to content

Commit 6a5c471

Browse files
committed
Pass mutation generation errors from subprocess to main process
1 parent 7b03f4b commit 6a5c471

File tree

11 files changed

+99
-26
lines changed

11 files changed

+99
-26
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Mutant files
22
e2e_projects/**/mutants
3+
/mutants
4+
tests/data/**/*.py.meta
35

46
*.py[cod]
57
examples/db.sqlite3

mutmut/__main__.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
Dict,
4848
List,
4949
Union,
50+
Optional,
5051
)
52+
import warnings
5153

5254
import click
5355
import libcst as cst
@@ -177,6 +179,13 @@ def __init__(self, pytest_args: list[str]) -> None:
177179
super().__init__(msg)
178180

179181

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+
180189
def copy_src_dir():
181190
for path in mutmut.config.paths_to_mutate:
182191
output_path: Path = Path('mutants') / path
@@ -186,21 +195,33 @@ def copy_src_dir():
186195
output_path.parent.mkdir(exist_ok=True, parents=True)
187196
shutil.copyfile(path, output_path)
188197

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
189203

190204
def create_mutants(max_children: int):
191205
with Pool(processes=max_children) as p:
192-
p.map(create_file_mutants, walk_source_files())
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
193211

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)
194217

195-
def create_file_mutants(path: Path):
196-
print(path)
197-
output_path = Path('mutants') / path
198-
makedirs(output_path.parent, exist_ok=True)
199-
200-
if mutmut.config.should_ignore_for_mutation(path):
201-
shutil.copy(path, output_path)
202-
else:
203-
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)
204225

205226

206227
def copy_also_copy_files():
@@ -216,23 +237,30 @@ def copy_also_copy_files():
216237
else:
217238
shutil.copytree(path, destination, dirs_exist_ok=True)
218239

219-
220-
def create_mutants_for_file(filename, output_path):
240+
def create_mutants_for_file(filename, output_path) -> FileMutationResult:
221241
input_stat = os.stat(filename)
242+
warnings: list[Warning] = []
222243

223244
with open(filename) as f:
224245
source = f.read()
225246

226247
with open(output_path, 'w') as out:
227-
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 = [], {}
228255

229256
# validate no syntax errors of mutants
230257
with open(output_path) as f:
231258
try:
232259
ast.parse(f.read())
233260
except (IndentationError, SyntaxError) as e:
234-
print(output_path, 'has invalid syntax: ', e)
235-
exit(1)
261+
invalid_syntax_error = InvalidGeneratedSyntaxException(output_path)
262+
invalid_syntax_error.__cause__ = e
263+
return FileMutationResult(warnings=warnings, error=invalid_syntax_error)
236264

237265
source_file_mutation_data = SourceFileMutationData(path=filename)
238266
module_name = strip_prefix(str(filename)[:-len(filename.suffix)].replace(os.sep, '.'), prefix='src.')
@@ -246,6 +274,7 @@ def create_mutants_for_file(filename, output_path):
246274
source_file_mutation_data.save()
247275

248276
os.utime(output_path, (input_stat.st_atime, input_stat.st_mtime))
277+
return FileMutationResult(warnings=warnings)
249278

250279

251280
def write_all_mutants_to_file(*, out, source, filename):

mutmut/file_mutation.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ def mutate_file_contents(filename: str, code: str) -> tuple[str, Sequence[str]]:
2525
"""Create mutations for `code` and merge them to a single mutated file with trampolines.
2626
2727
: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)
3329

3430
return combine_mutations_to_source(module, mutations)
3531

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""This file contains invalid python syntax"""
2+
3+
def foo():
4+
return 1 ///// 2
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 1 + 2
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 2 + 3
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 3 + 4
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 4 + 5
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def foo():
2+
return 5 + 6
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

0 commit comments

Comments
 (0)