Skip to content

Commit a071009

Browse files
committed
capture errors in test execution
1 parent f179814 commit a071009

File tree

9 files changed

+411
-59
lines changed

9 files changed

+411
-59
lines changed

python_tool_competition_2024/calculation/cli_runner.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,32 @@
3333

3434
@overload
3535
def run_command(
36-
config: Config, command: _COMMAND, *args: str, capture: Literal[True]
36+
config: Config,
37+
command: _COMMAND,
38+
*args: str,
39+
capture: Literal[True],
40+
show_output_on_error: bool = ...,
3741
) -> str:
3842
...
3943

4044

4145
@overload
4246
def run_command(
43-
config: Config, command: _COMMAND, *args: str, capture: Literal[False] = ...
47+
config: Config,
48+
command: _COMMAND,
49+
*args: str,
50+
capture: Literal[False] = ...,
51+
show_output_on_error: bool = ...,
4452
) -> None:
4553
...
4654

4755

4856
def run_command(
49-
config: Config, command: _COMMAND, *args: str, capture: Literal[True, False] = False
57+
config: Config,
58+
command: _COMMAND,
59+
*args: str,
60+
capture: Literal[True, False] = False,
61+
show_output_on_error: bool = True,
5062
) -> None | str:
5163
"""
5264
Run a command on the command line.
@@ -70,7 +82,8 @@ def run_command(
7082
with config.console.capture() as console_capture:
7183
output = _run_command(config, command, args)
7284
except CommandFailedError:
73-
config.console.out(console_capture.get(), highlight=False, end="")
85+
if show_output_on_error:
86+
config.console.out(console_capture.get(), highlight=False, end="")
7487
raise
7588
return output if capture else None
7689

python_tool_competition_2024/calculation/coverage_caluclator.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131

3232
from ..calculation.cli_runner import run_command
3333
from ..config import Config
34-
from ..errors import ConditionCoverageError, TargetNotFoundInCoveragesError
34+
from ..errors import (
35+
CommandFailedError,
36+
ConditionCoverageError,
37+
TargetNotFoundInCoveragesError,
38+
)
3539
from ..results import RatioResult
3640
from ..target_finder import Target
3741

@@ -53,20 +57,27 @@ def _generate_coverage_xml(target: Target, config: Config) -> Path:
5357
coverage_xml = config.coverages_dir / f"{target.source_module}.xml"
5458
coverage_xml.unlink(missing_ok=True)
5559
if target.test.exists():
56-
run_command(
57-
config,
58-
"pytest",
59-
str(target.test),
60-
f"--cov={target.source_module}",
61-
"--cov-branch",
62-
"--cov-report",
63-
f"xml:{coverage_xml}",
64-
"--color=yes",
65-
# reset options that are not desired
66-
"--cov-fail-under=0",
67-
"--override-ini=addopts=",
68-
"--override-ini=cache_dir=.pytest_competition_cache",
69-
)
60+
try:
61+
run_command(
62+
config,
63+
"pytest",
64+
str(target.test),
65+
f"--cov={target.source_module}",
66+
"--cov-branch",
67+
"--cov-report",
68+
f"xml:{coverage_xml}",
69+
"--color=yes",
70+
# reset options that are not desired
71+
"--cov-fail-under=0",
72+
"--override-ini=addopts=",
73+
"--override-ini=cache_dir=.pytest_competition_cache",
74+
show_output_on_error=False,
75+
)
76+
except CommandFailedError:
77+
msg = f"Could not run pytest for {target.source_module}."
78+
if not config.show_commands:
79+
msg = f"{msg} Add -vv to show the console output."
80+
config.console.print(msg, style="red")
7081

7182
# if the test was not generated or it does not import the source
7283
if not coverage_xml.exists():

python_tool_competition_2024/calculation/mutation_calculator/cosmic_ray_calculator.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import toml
2828

2929
from ...config import Config
30+
from ...errors import CommandFailedError
3031
from ...results import RatioResult
3132
from ...target_finder import Target
3233
from ..cli_runner import run_command
@@ -46,10 +47,23 @@ def calculate_mutation(target: Target, config: Config) -> RatioResult:
4647
run_command(
4748
config, "cosmic-ray", "init", str(files.config_file), str(files.db_file)
4849
)
49-
run_command(config, "cosmic-ray", "baseline", str(files.config_file))
50-
run_command(
51-
config, "cosmic-ray", "exec", str(files.config_file), str(files.db_file)
52-
)
50+
try:
51+
run_command(
52+
config,
53+
"cosmic-ray",
54+
"baseline",
55+
str(files.config_file),
56+
show_output_on_error=False,
57+
)
58+
except CommandFailedError:
59+
msg = f"Could not run mutation testing for {target.source_module}."
60+
if not config.show_commands:
61+
msg = f"{msg} Add -vv to show the console output."
62+
config.console.print(msg, style="red")
63+
else:
64+
run_command(
65+
config, "cosmic-ray", "exec", str(files.config_file), str(files.db_file)
66+
)
5367
return _gather_results(files, config)
5468

5569

python_tool_competition_2024/calculation/mutation_calculator/mutpy_calculator.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import re
2323

2424
from ...config import Config
25+
from ...errors import CommandFailedError
2526
from ...results import RatioResult
2627
from ...target_finder import Target
2728
from ..cli_runner import run_command
@@ -31,12 +32,30 @@
3132
_KILLED_REGEX = re.compile(
3233
r"\A\s+- killed: (?P<number>\d+) \((?P<percentage>\d+\.\d+%)\)\Z"
3334
)
35+
_EMPTY_MODULE = "typing"
3436

3537

3638
def calculate_mutation(target: Target, config: Config) -> RatioResult:
3739
"""Calculate mutation analysis using mutpy."""
38-
test_module = target.test_module if target.test.exists() else "typing"
39-
output = run_command(
40+
test_module = target.test_module if target.test.exists() else _EMPTY_MODULE
41+
try:
42+
output = _run_mutpy(target, config, test_module)
43+
except CommandFailedError:
44+
if test_module == _EMPTY_MODULE:
45+
raise
46+
msg = f"Could not run mutation testing for {target.source_module}."
47+
if not config.show_commands:
48+
msg = f"{msg} Add -vv to show the console output."
49+
config.console.print(msg, style="red")
50+
output = _run_mutpy(target, config, _EMPTY_MODULE)
51+
lines = tuple(output.splitlines())
52+
total = int(find_matching_line(_TOTAL_REGEX, lines).group("number"))
53+
killed = int(find_matching_line(_KILLED_REGEX, lines).group("number"))
54+
return RatioResult(total, killed)
55+
56+
57+
def _run_mutpy(target: Target, config: Config, test_module: str) -> str:
58+
return run_command(
4059
config,
4160
"mut.py",
4261
"--target",
@@ -46,8 +65,5 @@ def calculate_mutation(target: Target, config: Config) -> RatioResult:
4665
"--runner",
4766
"pytest",
4867
capture=True,
68+
show_output_on_error=False,
4969
)
50-
lines = tuple(output.splitlines())
51-
total = int(find_matching_line(_TOTAL_REGEX, lines).group("number"))
52-
killed = int(find_matching_line(_KILLED_REGEX, lines).group("number"))
53-
return RatioResult(total, killed)

tests/calculation/mutation_calculator/test_cosmic_ray_calculator.py

+115-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
calculate_mutation,
99
)
1010
from python_tool_competition_2024.config import Config
11+
from python_tool_competition_2024.errors import CommandFailedError
1112
from python_tool_competition_2024.results import RatioResult
1213
from python_tool_competition_2024.target_finder import find_targets
1314

@@ -72,6 +73,99 @@ def test_cosmic_ray_calculator(tmp_path: Path) -> None:
7273
}
7374

7475

76+
def test_cosmic_ray_calculator_with_failing_baseline(tmp_path: Path) -> None:
77+
with mock.patch(
78+
"python_tool_competition_2024.calculation.mutation_calculator.cosmic_ray_calculator.run_command"
79+
) as run_command_mock:
80+
run_command_mock.side_effect = _OutputCounter(fail_baseline=True)
81+
mock.seal(run_command_mock)
82+
generated_tests = tmp_path / "dummy" / "generated_tests"
83+
generated_tests.mkdir(parents=True)
84+
(generated_tests / "test_example1.py").touch()
85+
(generated_tests / "test_example2.py").touch()
86+
config = get_test_config(
87+
show_commands=False,
88+
show_failures=False,
89+
targets_dir=TARGETS_DIR,
90+
results_dir=tmp_path,
91+
)
92+
targets = find_targets(config)
93+
with config.console.capture() as capture:
94+
assert {
95+
target.source_module: calculate_mutation(target, config)
96+
for target in targets
97+
} == {
98+
"example1": RatioResult(10, 0),
99+
"example2": RatioResult(11, 1),
100+
"sub_example": RatioResult(12, 2),
101+
"sub_example.example3": RatioResult(13, 3),
102+
}
103+
assert tuple(capture.get().splitlines()) == tuple(
104+
(
105+
f"Could not run mutation testing for {module}. "
106+
"Add -vv to show the console output."
107+
)
108+
for module in (
109+
"example1",
110+
"example2",
111+
"sub_example",
112+
"sub_example.example3",
113+
)
114+
)
115+
116+
assert run_command_mock.call_args_list == [
117+
*_cr_calls(config, "example1", skip_exec=True),
118+
*_cr_calls(config, "example2", skip_exec=True),
119+
*_cr_calls(config, "sub_example", skip_exec=True),
120+
*_cr_calls(config, "sub_example.example3", skip_exec=True),
121+
]
122+
123+
124+
def test_cosmic_ray_calculator_with_failing_baseline_and_output(tmp_path: Path) -> None:
125+
with mock.patch(
126+
"python_tool_competition_2024.calculation.mutation_calculator.cosmic_ray_calculator.run_command"
127+
) as run_command_mock:
128+
run_command_mock.side_effect = _OutputCounter(fail_baseline=True)
129+
mock.seal(run_command_mock)
130+
generated_tests = tmp_path / "dummy" / "generated_tests"
131+
generated_tests.mkdir(parents=True)
132+
(generated_tests / "test_example1.py").touch()
133+
(generated_tests / "test_example2.py").touch()
134+
config = get_test_config(
135+
show_commands=True,
136+
show_failures=False,
137+
targets_dir=TARGETS_DIR,
138+
results_dir=tmp_path,
139+
)
140+
targets = find_targets(config)
141+
with config.console.capture() as capture:
142+
assert {
143+
target.source_module: calculate_mutation(target, config)
144+
for target in targets
145+
} == {
146+
"example1": RatioResult(10, 0),
147+
"example2": RatioResult(11, 1),
148+
"sub_example": RatioResult(12, 2),
149+
"sub_example.example3": RatioResult(13, 3),
150+
}
151+
assert tuple(capture.get().splitlines()) == tuple(
152+
f"Could not run mutation testing for {module}."
153+
for module in (
154+
"example1",
155+
"example2",
156+
"sub_example",
157+
"sub_example.example3",
158+
)
159+
)
160+
161+
assert run_command_mock.call_args_list == [
162+
*_cr_calls(config, "example1", skip_exec=True),
163+
*_cr_calls(config, "example2", skip_exec=True),
164+
*_cr_calls(config, "sub_example", skip_exec=True),
165+
*_cr_calls(config, "sub_example.example3", skip_exec=True),
166+
]
167+
168+
75169
def test_gather_results_not_started() -> None:
76170
with mock.patch(
77171
"python_tool_competition_2024.calculation.mutation_calculator.cosmic_ray_calculator.run_command"
@@ -107,16 +201,28 @@ def test_gather_results_not_completed() -> None:
107201
)
108202

109203

110-
def _cr_calls(config: Config, target: str) -> tuple[mock._Call, ...]:
204+
def _cr_calls(
205+
config: Config, target: str, *, skip_exec: bool = False
206+
) -> tuple[mock._Call, ...]:
111207
cr_dir = config.results_dir / "cosmic_ray"
112208
config_file = cr_dir / f"{target}.toml"
113209
db_file = cr_dir / f"{target}.sqlite"
114-
return (
210+
calls: tuple[mock._Call, ...] = (
115211
mock.call(config, "cosmic-ray", "init", str(config_file), str(db_file)),
116-
mock.call(config, "cosmic-ray", "baseline", str(config_file)),
117-
mock.call(config, "cosmic-ray", "exec", str(config_file), str(db_file)),
118-
mock.call(config, "cr-report", str(db_file), capture=True),
212+
mock.call(
213+
config,
214+
"cosmic-ray",
215+
"baseline",
216+
str(config_file),
217+
show_output_on_error=False,
218+
),
119219
)
220+
if not skip_exec:
221+
calls = (
222+
*calls,
223+
mock.call(config, "cosmic-ray", "exec", str(config_file), str(db_file)),
224+
)
225+
return (*calls, mock.call(config, "cr-report", str(db_file), capture=True))
120226

121227

122228
def _cr_config(target: Path, test_file: Path | None) -> str:
@@ -136,11 +242,14 @@ def _cr_config(target: Path, test_file: Path | None) -> str:
136242

137243

138244
class _OutputCounter:
139-
def __init__(self) -> None:
245+
def __init__(self, *, fail_baseline: bool = False) -> None:
140246
self._total_count = 9
141247
self._successful_count = -1
248+
self._fail_baseline = fail_baseline
142249

143250
def __call__(self, _config: Config, *args: str, **_kwargs: object) -> str:
251+
if self._fail_baseline and args[0:2] == ("cosmic-ray", "baseline"):
252+
raise CommandFailedError(args)
144253
if args[0] != "cr-report":
145254
return ""
146255
self._total_count += 1

0 commit comments

Comments
 (0)