poodle 1.0.0__py3-none-any.whl
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.
- poodle/__init__.py +29 -0
- poodle/cli.py +58 -0
- poodle/config.py +480 -0
- poodle/core.py +63 -0
- poodle/data_types/__init__.py +5 -0
- poodle/data_types/data.py +129 -0
- poodle/data_types/interfaces.py +139 -0
- poodle/data_types/work.py +68 -0
- poodle/mutate.py +213 -0
- poodle/mutators/__init__.py +7 -0
- poodle/mutators/calls.py +113 -0
- poodle/mutators/compare.py +90 -0
- poodle/mutators/constant.py +100 -0
- poodle/mutators/operators.py +140 -0
- poodle/mutators/unary_op.py +40 -0
- poodle/report.py +33 -0
- poodle/reporters/__init__.py +3 -0
- poodle/reporters/basic.py +99 -0
- poodle/run.py +183 -0
- poodle/runners/__init__.py +1 -0
- poodle/runners/command_line.py +77 -0
- poodle/util.py +80 -0
- poodle-1.0.0.dist-info/LICENSE +21 -0
- poodle-1.0.0.dist-info/METADATA +115 -0
- poodle-1.0.0.dist-info/RECORD +28 -0
- poodle-1.0.0.dist-info/WHEEL +5 -0
- poodle-1.0.0.dist-info/entry_points.txt +2 -0
- poodle-1.0.0.dist-info/top_level.txt +1 -0
poodle/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Poodle Mutation Test Tool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PoodleInputError(ValueError):
|
|
13
|
+
"""An input value from Command Line, poodle_config.py, or a config file was invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PoodleTrialRunError(Exception):
|
|
17
|
+
"""An unexpected error occurred when running a mutation trial or clean run."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import tomllib # type: ignore [import-not-found]
|
|
22
|
+
except ModuleNotFoundError: # < py3.11
|
|
23
|
+
import tomli as tomllib # type: ignore [no-redef]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
poodle_config: Any = None
|
|
27
|
+
with suppress(ImportError):
|
|
28
|
+
sys.path.append(str(Path.cwd()))
|
|
29
|
+
poodle_config = importlib.import_module("poodle_config")
|
poodle/cli.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Command Line Interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from . import PoodleInputError, core
|
|
11
|
+
from .config import build_config
|
|
12
|
+
|
|
13
|
+
CONTEXT_SETTINGS = {
|
|
14
|
+
"max_content_width": 120,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command(context_settings=CONTEXT_SETTINGS)
|
|
19
|
+
@click.argument("sources", type=click.Path(exists=True, path_type=Path), nargs=-1)
|
|
20
|
+
@click.option("-c", "config_file", help="Configuration File.", type=click.Path(exists=True, path_type=Path))
|
|
21
|
+
@click.option("-q", "quiet", help="Quiet mode: q, qq, or qq", count=True)
|
|
22
|
+
@click.option("-v", "verbose", help="Verbose mode: v, vv, or vvv", count=True)
|
|
23
|
+
@click.option("-w", "workers", help="Maximum number of parallel workers.", type=int)
|
|
24
|
+
@click.option("--exclude", help="Add a regex exclude file filter. Multiple allowed.", multiple=True)
|
|
25
|
+
@click.option("--only", help="Glob pattern for files to mutate. Multiple allowed.", multiple=True)
|
|
26
|
+
def main(
|
|
27
|
+
sources: tuple[Path],
|
|
28
|
+
config_file: Path | None,
|
|
29
|
+
quiet: int,
|
|
30
|
+
verbose: int,
|
|
31
|
+
workers: int | None,
|
|
32
|
+
exclude: tuple[str],
|
|
33
|
+
only: tuple[str],
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Poodle Mutation Test Tool."""
|
|
36
|
+
try:
|
|
37
|
+
config = build_config(sources, config_file, quiet, verbose, workers, exclude, only)
|
|
38
|
+
except PoodleInputError as err:
|
|
39
|
+
click.echo(err.args)
|
|
40
|
+
sys.exit(4)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
core.main(config)
|
|
44
|
+
except KeyboardInterrupt:
|
|
45
|
+
click.echo("Aborted due to Keyboard Interrupt!")
|
|
46
|
+
sys.exit(2)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# pytest return codes
|
|
50
|
+
# Exit code 0: All tests were collected and passed successfully
|
|
51
|
+
# Exit code 1: Tests were collected and run but some of the tests failed
|
|
52
|
+
# Exit code 2: Test execution was interrupted by the user
|
|
53
|
+
# Exit code 3: Internal error happened while executing tests
|
|
54
|
+
# Exit code 4: pytest command line usage error
|
|
55
|
+
# Exit code 5: No tests were collected
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
poodle/config.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""Resolve configuration options and build PoodleConfig object."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from wcmatch import glob
|
|
12
|
+
|
|
13
|
+
from . import PoodleInputError, poodle_config, tomllib
|
|
14
|
+
from .data_types import PoodleConfig
|
|
15
|
+
|
|
16
|
+
default_source_folders = [Path("src"), Path("lib")]
|
|
17
|
+
|
|
18
|
+
default_log_format = "%(levelname)s [%(process)d] %(name)s.%(funcName)s:%(lineno)d - %(message)s"
|
|
19
|
+
default_log_level = logging.WARN
|
|
20
|
+
|
|
21
|
+
default_file_flags = glob.GLOBSTAR | glob.NODIR
|
|
22
|
+
default_file_filters = ["test_*.py", "*_test.py"]
|
|
23
|
+
default_file_copy_flags = glob.GLOBSTAR | glob.NODIR
|
|
24
|
+
default_file_copy_filters = ["test_*.py", "*_test.py", "__pycache__/**"]
|
|
25
|
+
default_work_folder = Path(".poodle-temp")
|
|
26
|
+
|
|
27
|
+
default_mutator_opts: dict[str, Any] = {}
|
|
28
|
+
|
|
29
|
+
default_min_timeout = 10
|
|
30
|
+
default_timeout_multiplier = 10
|
|
31
|
+
default_runner = "command_line"
|
|
32
|
+
default_runner_opts: dict[str, Any] = {"command_line": "pytest -x --assert=plain -o pythonpath="}
|
|
33
|
+
|
|
34
|
+
default_reporters = ["summary", "not_found"]
|
|
35
|
+
default_reporter_opts: dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def default_max_workers() -> int:
|
|
39
|
+
"""Calculate Default for max_workers as one less than available processors."""
|
|
40
|
+
if hasattr(os, "sched_getaffinity"):
|
|
41
|
+
return len(os.sched_getaffinity(0)) - 1
|
|
42
|
+
cpu_count = os.cpu_count() or 1 # nomut: Number
|
|
43
|
+
if cpu_count > 1:
|
|
44
|
+
return cpu_count - 1
|
|
45
|
+
return cpu_count
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_config( # noqa: PLR0913
|
|
49
|
+
cmd_sources: tuple[Path],
|
|
50
|
+
cmd_config_file: Path | None,
|
|
51
|
+
cmd_quiet: int,
|
|
52
|
+
cmd_verbose: int,
|
|
53
|
+
cmd_max_workers: int | None,
|
|
54
|
+
cmd_excludes: tuple[str],
|
|
55
|
+
cmd_only_files: tuple[str],
|
|
56
|
+
) -> PoodleConfig:
|
|
57
|
+
"""Build PoodleConfig object."""
|
|
58
|
+
config_file_path = get_config_file_path(cmd_config_file)
|
|
59
|
+
config_file_data = get_config_file_data(config_file_path)
|
|
60
|
+
|
|
61
|
+
log_format = get_str_from_config("log_format", config_file_data, default=default_log_format)
|
|
62
|
+
log_level: int | str = get_any_from_config(
|
|
63
|
+
"log_level",
|
|
64
|
+
config_file_data,
|
|
65
|
+
default=default_log_level,
|
|
66
|
+
command_line=get_cmd_line_log_level(cmd_quiet, cmd_verbose),
|
|
67
|
+
)
|
|
68
|
+
logging.basicConfig(format=log_format, level=log_level)
|
|
69
|
+
|
|
70
|
+
file_filters = get_str_list_from_config("file_filters", config_file_data, default=default_file_filters)
|
|
71
|
+
# TODO: append file excludes and append py excludes
|
|
72
|
+
# file_filters += get_str_list_from_config("exclude", config_file_data, default=[]) # noqa: ERA001
|
|
73
|
+
file_filters += cmd_excludes
|
|
74
|
+
|
|
75
|
+
return PoodleConfig(
|
|
76
|
+
config_file=config_file_path,
|
|
77
|
+
source_folders=get_source_folders(cmd_sources, config_file_data),
|
|
78
|
+
only_files=get_str_list_from_config("only_files", config_file_data, default=[], command_line=cmd_only_files),
|
|
79
|
+
file_flags=get_int_from_config("file_flags", config_file_data, default=default_file_flags),
|
|
80
|
+
file_filters=file_filters,
|
|
81
|
+
file_copy_flags=get_int_from_config("file_copy_flags", config_file_data, default=default_file_copy_flags),
|
|
82
|
+
file_copy_filters=get_str_list_from_config(
|
|
83
|
+
"file_copy_filters",
|
|
84
|
+
config_file_data,
|
|
85
|
+
default=default_file_copy_filters,
|
|
86
|
+
),
|
|
87
|
+
work_folder=get_path_from_config("work_folder", config_file_data, default=default_work_folder),
|
|
88
|
+
max_workers=get_int_from_config(
|
|
89
|
+
"max_workers",
|
|
90
|
+
config_file_data,
|
|
91
|
+
default=default_max_workers(),
|
|
92
|
+
command_line=cmd_max_workers,
|
|
93
|
+
),
|
|
94
|
+
log_format=log_format,
|
|
95
|
+
log_level=log_level,
|
|
96
|
+
echo_enabled=get_bool_from_config(
|
|
97
|
+
"echo_enabled",
|
|
98
|
+
config_file_data,
|
|
99
|
+
default=True,
|
|
100
|
+
command_line=get_cmd_line_echo_enabled(cmd_quiet),
|
|
101
|
+
),
|
|
102
|
+
echo_no_color=get_bool_from_config("echo_no_color", config_file_data),
|
|
103
|
+
mutator_opts=get_dict_from_config("mutator_opts", config_file_data, default=default_mutator_opts),
|
|
104
|
+
skip_mutators=get_str_list_from_config("skip_mutators", config_file_data, default=[]),
|
|
105
|
+
add_mutators=get_any_list_from_config("add_mutators", config_file_data),
|
|
106
|
+
min_timeout=get_int_from_config("min_timeout", config_file_data) or default_min_timeout,
|
|
107
|
+
timeout_multiplier=get_int_from_config("timeout_multiplier", config_file_data) or default_timeout_multiplier,
|
|
108
|
+
runner=get_str_from_config("runner", config_file_data, default=default_runner),
|
|
109
|
+
runner_opts=get_dict_from_config("runner_opts", config_file_data, default=default_runner_opts),
|
|
110
|
+
reporters=get_str_list_from_config("reporters", config_file_data, default=default_reporters),
|
|
111
|
+
reporter_opts=get_dict_from_config("reporter_opts", config_file_data, default=default_reporter_opts),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_cmd_line_log_level(cmd_quiet: int, cmd_verbose: int) -> int | None:
|
|
116
|
+
"""Map verbosity input to logging level."""
|
|
117
|
+
if cmd_quiet >= 3:
|
|
118
|
+
return logging.CRITICAL
|
|
119
|
+
if cmd_quiet == 2:
|
|
120
|
+
return logging.ERROR
|
|
121
|
+
if cmd_quiet == 1:
|
|
122
|
+
return logging.WARN
|
|
123
|
+
|
|
124
|
+
if cmd_verbose == 1:
|
|
125
|
+
return logging.INFO
|
|
126
|
+
if cmd_verbose == 2:
|
|
127
|
+
return logging.DEBUG
|
|
128
|
+
if cmd_verbose >= 3:
|
|
129
|
+
return logging.NOTSET
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_cmd_line_echo_enabled(cmd_quiet: int) -> bool | None:
|
|
135
|
+
"""Map verbosity input to enable/disable echo statements."""
|
|
136
|
+
return cmd_quiet < 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_config_file_path(config_file: Path | None) -> Path | None:
|
|
140
|
+
"""Identify which configuration file to use.
|
|
141
|
+
|
|
142
|
+
Checks in this order, first value is used.
|
|
143
|
+
Command Line, poodle.toml, pyproject.toml.
|
|
144
|
+
Returns None if no config file found.
|
|
145
|
+
"""
|
|
146
|
+
if config_file:
|
|
147
|
+
if not config_file.is_file():
|
|
148
|
+
msg = f"Config file not found: '{config_file}'"
|
|
149
|
+
raise PoodleInputError(msg)
|
|
150
|
+
return config_file
|
|
151
|
+
|
|
152
|
+
if hasattr(poodle_config, "config_file"):
|
|
153
|
+
config_file = Path(poodle_config.config_file)
|
|
154
|
+
if not config_file.is_file():
|
|
155
|
+
msg = f"config_file not found: '{poodle_config.config_file}'"
|
|
156
|
+
raise PoodleInputError(msg)
|
|
157
|
+
return config_file
|
|
158
|
+
|
|
159
|
+
files = [
|
|
160
|
+
"poodle.toml",
|
|
161
|
+
"pyproject.toml",
|
|
162
|
+
] # TODO: ["poodle.toml", "tox.ini", "setup.cfg", "pyproject.toml"]
|
|
163
|
+
|
|
164
|
+
for file in files:
|
|
165
|
+
path = Path(file)
|
|
166
|
+
if path.is_file():
|
|
167
|
+
return path
|
|
168
|
+
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_config_file_data(config_file: Path | None) -> dict:
|
|
173
|
+
"""Retrieve Poodle configuration from specified Config File."""
|
|
174
|
+
if not config_file:
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
if config_file.suffix == ".toml":
|
|
178
|
+
return get_config_file_data_toml(config_file)
|
|
179
|
+
|
|
180
|
+
# TODO: tox.ini and setup.cfg
|
|
181
|
+
# https://tox.wiki/en/3.24.5/config.html
|
|
182
|
+
|
|
183
|
+
msg = f"Config file type not supported: --config_file='{config_file}'"
|
|
184
|
+
raise PoodleInputError(msg)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_config_file_data_toml(config_file: Path) -> dict:
|
|
188
|
+
"""Retrieve Poodle configuration from a 'toml' Config File."""
|
|
189
|
+
config_data = tomllib.load(config_file.open(mode="rb"))
|
|
190
|
+
config_data = config_data.get("tool", config_data)
|
|
191
|
+
return config_data.get("poodle", {})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_source_folders(command_line_sources: tuple[Path], config_data: dict) -> list[Path]:
|
|
195
|
+
"""Retrieve list of source folders that contain files to mutate.
|
|
196
|
+
|
|
197
|
+
Verifies that all returned values are existing directories.
|
|
198
|
+
"""
|
|
199
|
+
source_folders = get_path_list_from_config(
|
|
200
|
+
option_name="source_folders",
|
|
201
|
+
config_data=config_data,
|
|
202
|
+
command_line=command_line_sources,
|
|
203
|
+
default=[source for source in default_source_folders if source.is_dir()],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if not source_folders:
|
|
207
|
+
raise PoodleInputError("No source folder found to mutate.")
|
|
208
|
+
|
|
209
|
+
for source in source_folders:
|
|
210
|
+
if not source.is_dir():
|
|
211
|
+
msg = f"Source '{source}' must be a folder."
|
|
212
|
+
raise PoodleInputError(msg)
|
|
213
|
+
|
|
214
|
+
return source_folders
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_bool_from_config(
|
|
218
|
+
option_name: str,
|
|
219
|
+
config_data: dict,
|
|
220
|
+
default: bool | None = None,
|
|
221
|
+
command_line: bool | str | None = None,
|
|
222
|
+
) -> bool | None:
|
|
223
|
+
"""Retrieve Config Option that should be a Boolean.
|
|
224
|
+
|
|
225
|
+
Retrieve highest priority value from config sources.
|
|
226
|
+
"""
|
|
227
|
+
value, _ = get_option_from_config(option_name=option_name, config_data=config_data, command_line=command_line)
|
|
228
|
+
|
|
229
|
+
if isinstance(value, bool):
|
|
230
|
+
return value
|
|
231
|
+
|
|
232
|
+
if isinstance(value, str):
|
|
233
|
+
return value.upper() == "TRUE"
|
|
234
|
+
|
|
235
|
+
return default
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_path_from_config(
|
|
239
|
+
option_name: str,
|
|
240
|
+
config_data: dict,
|
|
241
|
+
default: Path,
|
|
242
|
+
command_line: Path | None = None,
|
|
243
|
+
) -> Path:
|
|
244
|
+
"""Retrieve Config Option that should be a String.
|
|
245
|
+
|
|
246
|
+
Retrieve highest priority value from config sources.
|
|
247
|
+
"""
|
|
248
|
+
value, source = get_option_from_config(option_name=option_name, config_data=config_data, command_line=command_line)
|
|
249
|
+
|
|
250
|
+
if value is None:
|
|
251
|
+
return default
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
return Path(value)
|
|
255
|
+
except TypeError:
|
|
256
|
+
msg = f"{option_name} from {source} must be a valid StrPath"
|
|
257
|
+
raise PoodleInputError(msg) from None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_path_list_from_config(
|
|
261
|
+
option_name: str,
|
|
262
|
+
config_data: dict,
|
|
263
|
+
default: list[Path] | None = None,
|
|
264
|
+
command_line: tuple[Path] | None = None,
|
|
265
|
+
) -> list[Path]:
|
|
266
|
+
"""Retrieve Config Option that should be a List of Paths.
|
|
267
|
+
|
|
268
|
+
Retrieve highest priority value from config sources.
|
|
269
|
+
If input was a single Path, return as a list of Paths.
|
|
270
|
+
Convert input Iterable to List.
|
|
271
|
+
"""
|
|
272
|
+
default_fix = default or []
|
|
273
|
+
command_line_fix = command_line or ()
|
|
274
|
+
|
|
275
|
+
values, source = get_option_from_config(
|
|
276
|
+
option_name=option_name,
|
|
277
|
+
config_data=config_data,
|
|
278
|
+
command_line=command_line_fix,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not values:
|
|
282
|
+
return default_fix
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
if isinstance(values, Path):
|
|
286
|
+
return [values]
|
|
287
|
+
|
|
288
|
+
if isinstance(values, str):
|
|
289
|
+
return [Path(values)]
|
|
290
|
+
|
|
291
|
+
return [Path(value) for value in values]
|
|
292
|
+
except TypeError:
|
|
293
|
+
msg = f"{option_name} from {source} must be a valid Iterable[StrPath]"
|
|
294
|
+
raise PoodleInputError(msg) from None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_any_from_config(
|
|
298
|
+
option_name: str,
|
|
299
|
+
config_data: dict,
|
|
300
|
+
default: Any = None, # noqa: ANN401
|
|
301
|
+
command_line: Any | None = None, # noqa: ANN401
|
|
302
|
+
) -> Any: # noqa: ANN401
|
|
303
|
+
"""Retrieve Config Option that can by any type.
|
|
304
|
+
|
|
305
|
+
Retrieve highest priority value from config sources.
|
|
306
|
+
"""
|
|
307
|
+
value, _ = get_option_from_config(option_name=option_name, config_data=config_data, command_line=command_line)
|
|
308
|
+
|
|
309
|
+
if value is None:
|
|
310
|
+
return default
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def get_any_list_from_config(
|
|
315
|
+
option_name: str,
|
|
316
|
+
config_data: dict,
|
|
317
|
+
default: list[Any] | None = None,
|
|
318
|
+
command_line: tuple[Any] | None = None,
|
|
319
|
+
) -> list[Any]:
|
|
320
|
+
"""Retrieve Config Option that should be a List of any types.
|
|
321
|
+
|
|
322
|
+
Retrieve highest priority value from config sources.
|
|
323
|
+
Convert input Iterable to List.
|
|
324
|
+
"""
|
|
325
|
+
default_fix = default or []
|
|
326
|
+
command_line_fix = command_line or ()
|
|
327
|
+
|
|
328
|
+
values, _ = get_option_from_config(
|
|
329
|
+
option_name=option_name,
|
|
330
|
+
config_data=config_data,
|
|
331
|
+
command_line=command_line_fix,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if values is None:
|
|
335
|
+
return default_fix
|
|
336
|
+
|
|
337
|
+
if isinstance(values, str):
|
|
338
|
+
return [values]
|
|
339
|
+
|
|
340
|
+
if isinstance(values, Iterable):
|
|
341
|
+
return list(values)
|
|
342
|
+
|
|
343
|
+
return [values]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def get_int_from_config(
|
|
347
|
+
option_name: str,
|
|
348
|
+
config_data: dict,
|
|
349
|
+
default: int | None = None,
|
|
350
|
+
command_line: int | None = None,
|
|
351
|
+
) -> int | None:
|
|
352
|
+
"""Retrieve Config Option that should be an int or None.
|
|
353
|
+
|
|
354
|
+
Retrieve highest priority value from config sources.
|
|
355
|
+
"""
|
|
356
|
+
value, source = get_option_from_config(option_name=option_name, config_data=config_data, command_line=command_line)
|
|
357
|
+
|
|
358
|
+
if value is None:
|
|
359
|
+
return default
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
return int(value)
|
|
363
|
+
except ValueError:
|
|
364
|
+
msg = f"{option_name} from {source} must be a valid int"
|
|
365
|
+
raise PoodleInputError(msg) from None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def get_str_from_config(
|
|
369
|
+
option_name: str,
|
|
370
|
+
config_data: dict,
|
|
371
|
+
default: str = "",
|
|
372
|
+
command_line: str = "",
|
|
373
|
+
) -> str:
|
|
374
|
+
"""Retrieve Config Option that should be a String.
|
|
375
|
+
|
|
376
|
+
Retrieve highest priority value from config sources.
|
|
377
|
+
"""
|
|
378
|
+
value, _ = get_option_from_config(option_name=option_name, config_data=config_data, command_line=command_line)
|
|
379
|
+
|
|
380
|
+
if value is None:
|
|
381
|
+
return default
|
|
382
|
+
|
|
383
|
+
return str(value)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_str_list_from_config(
|
|
387
|
+
option_name: str,
|
|
388
|
+
config_data: dict,
|
|
389
|
+
default: list[str] | None = None,
|
|
390
|
+
command_line: str | tuple[str] | None = None,
|
|
391
|
+
) -> list[str]:
|
|
392
|
+
"""Retrieve Config Option that should be a List of Strings.
|
|
393
|
+
|
|
394
|
+
Retrieve highest priority value from config sources.
|
|
395
|
+
If input was a single string, return as a list of strings.
|
|
396
|
+
Convert input Iterable to List.
|
|
397
|
+
"""
|
|
398
|
+
default_fix = default or []
|
|
399
|
+
command_line_fix = command_line or ()
|
|
400
|
+
|
|
401
|
+
values, source = get_option_from_config(
|
|
402
|
+
option_name=option_name,
|
|
403
|
+
config_data=config_data,
|
|
404
|
+
command_line=command_line_fix,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if not values:
|
|
408
|
+
return default_fix
|
|
409
|
+
|
|
410
|
+
if isinstance(values, str):
|
|
411
|
+
return [values]
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
return [str(value) for value in values]
|
|
415
|
+
except TypeError:
|
|
416
|
+
msg = f"{option_name} from {source} must be a valid Iterable[str]"
|
|
417
|
+
raise PoodleInputError(msg) from None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def get_option_from_config(
|
|
421
|
+
option_name: str,
|
|
422
|
+
config_data: dict,
|
|
423
|
+
command_line: Any, # noqa: ANN401
|
|
424
|
+
) -> tuple[Any | None, str | None]:
|
|
425
|
+
"""Retrieve Config Option of any type.
|
|
426
|
+
|
|
427
|
+
Check sources in priority order, and return the first one found.
|
|
428
|
+
|
|
429
|
+
Priority Order: Command Line, poodle_config.py, config file
|
|
430
|
+
|
|
431
|
+
Returns: Identified Config value, Source Name
|
|
432
|
+
"""
|
|
433
|
+
if command_line or command_line is False:
|
|
434
|
+
return command_line, "Command Line"
|
|
435
|
+
|
|
436
|
+
if hasattr(poodle_config, option_name):
|
|
437
|
+
return getattr(poodle_config, option_name), "poodle_config.py"
|
|
438
|
+
|
|
439
|
+
if option_name in config_data:
|
|
440
|
+
return config_data[option_name], "config file"
|
|
441
|
+
|
|
442
|
+
return None, None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_dict_from_config(
|
|
446
|
+
option_name: str,
|
|
447
|
+
config_data: dict,
|
|
448
|
+
default: dict | None = None,
|
|
449
|
+
command_line: dict | None = None,
|
|
450
|
+
) -> dict:
|
|
451
|
+
"""Merge Key-Value pairs from Config sources to a dict.
|
|
452
|
+
|
|
453
|
+
Builds dict by starting with values from lowest priority source.
|
|
454
|
+
Then 'updating' them with values from higher priority sources.
|
|
455
|
+
|
|
456
|
+
Priority Order: Command Line, poodle_config.py, config file, defaults
|
|
457
|
+
"""
|
|
458
|
+
default = default or {}
|
|
459
|
+
command_line = command_line or {}
|
|
460
|
+
|
|
461
|
+
option_value = default
|
|
462
|
+
|
|
463
|
+
if option_name in config_data:
|
|
464
|
+
try:
|
|
465
|
+
option_value.update(config_data[option_name])
|
|
466
|
+
except ValueError:
|
|
467
|
+
msg = f"{option_name} in config file must be a valid dict"
|
|
468
|
+
raise PoodleInputError(msg) from None
|
|
469
|
+
|
|
470
|
+
if hasattr(poodle_config, option_name):
|
|
471
|
+
try:
|
|
472
|
+
option_value.update(getattr(poodle_config, option_name))
|
|
473
|
+
except ValueError:
|
|
474
|
+
msg = f"poodle_config.{option_name} must be a valid dict"
|
|
475
|
+
raise PoodleInputError(msg) from None
|
|
476
|
+
|
|
477
|
+
if command_line:
|
|
478
|
+
option_value.update(command_line)
|
|
479
|
+
|
|
480
|
+
return option_value
|
poodle/core.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Poodle core run process."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from . import PoodleInputError, PoodleTrialRunError
|
|
12
|
+
from .data_types import MutantTrial, PoodleConfig, PoodleWork
|
|
13
|
+
from .mutate import create_mutants_for_all_mutators, initialize_mutators
|
|
14
|
+
from .report import generate_reporters
|
|
15
|
+
from .run import clean_run_each_source_folder, get_runner, run_mutant_trails
|
|
16
|
+
from .util import create_temp_zips, pprint_str
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(config: PoodleConfig) -> None:
|
|
25
|
+
"""Poodle core run process."""
|
|
26
|
+
try:
|
|
27
|
+
work = PoodleWork(config) # sets logging defaults
|
|
28
|
+
logger.info("\n%s", pprint_str(config))
|
|
29
|
+
|
|
30
|
+
if config.work_folder.exists():
|
|
31
|
+
logger.info("delete %s", config.work_folder)
|
|
32
|
+
shutil.rmtree(config.work_folder)
|
|
33
|
+
|
|
34
|
+
create_temp_zips(work)
|
|
35
|
+
|
|
36
|
+
work.mutators = initialize_mutators(work)
|
|
37
|
+
work.runner = get_runner(config)
|
|
38
|
+
work.reporters = list(generate_reporters(config))
|
|
39
|
+
|
|
40
|
+
mutants = create_mutants_for_all_mutators(work)
|
|
41
|
+
work.echo(f"Identified {len(mutants)} mutants")
|
|
42
|
+
|
|
43
|
+
clean_run_results = clean_run_each_source_folder(work)
|
|
44
|
+
timeout = calc_timeout(config, clean_run_results)
|
|
45
|
+
results = run_mutant_trails(work, mutants, timeout)
|
|
46
|
+
|
|
47
|
+
for reporter in work.reporters:
|
|
48
|
+
reporter(config=config, echo=work.echo, testing_results=results)
|
|
49
|
+
|
|
50
|
+
logger.info("delete %s", config.work_folder)
|
|
51
|
+
shutil.rmtree(config.work_folder)
|
|
52
|
+
except PoodleInputError as err:
|
|
53
|
+
for arg in err.args:
|
|
54
|
+
click.echo(arg)
|
|
55
|
+
except PoodleTrialRunError as err:
|
|
56
|
+
for arg in err.args:
|
|
57
|
+
click.echo(arg)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def calc_timeout(config: PoodleConfig, clean_run_results: dict[Path, MutantTrial]) -> float:
|
|
61
|
+
"""Determine timeout value to use in runner."""
|
|
62
|
+
max_clean_run = max([trial.duration for trial in clean_run_results.values()])
|
|
63
|
+
return max(float(max_clean_run) * config.timeout_multiplier, config.min_timeout)
|