sundgren 0.1.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.
- sundgren/__init__.py +3 -0
- sundgren/__main__.py +3 -0
- sundgren/cli.py +459 -0
- sundgren/core/__init__.py +1 -0
- sundgren/core/diff.py +536 -0
- sundgren/core/hashing.py +21 -0
- sundgren/core/ingest/__init__.py +1 -0
- sundgren/core/ingest/base.py +11 -0
- sundgren/core/ingest/lcov.py +211 -0
- sundgren/core/junit.py +81 -0
- sundgren/core/linemap.py +103 -0
- sundgren/core/model.py +120 -0
- sundgren/core/normalize.py +101 -0
- sundgren/core/patch.py +318 -0
- sundgren/core/report/__init__.py +1 -0
- sundgren/core/report/delta.schema.json +420 -0
- sundgren/core/report/json_report.py +156 -0
- sundgren/core/report/text_report.py +98 -0
- sundgren/core/rollup.py +188 -0
- sundgren/core/symbols.py +90 -0
- sundgren/lang/__init__.py +1 -0
- sundgren/lang/python/__init__.py +13 -0
- sundgren/lang/python/presets.toml +15 -0
- sundgren/lang/python/selector.py +46 -0
- sundgren/lang/python/symbolizer.py +74 -0
- sundgren/lang/rust/__init__.py +7 -0
- sundgren/lang/rust/presets.toml +14 -0
- sundgren/run/__init__.py +1 -0
- sundgren/run/guards.py +26 -0
- sundgren/run/presets.py +144 -0
- sundgren/run/runner.py +164 -0
- sundgren/run/selectors.py +61 -0
- sundgren/run/template.py +39 -0
- sundgren/run/workspace.py +180 -0
- sundgren-0.1.0.dist-info/METADATA +125 -0
- sundgren-0.1.0.dist-info/RECORD +39 -0
- sundgren-0.1.0.dist-info/WHEEL +4 -0
- sundgren-0.1.0.dist-info/entry_points.txt +2 -0
- sundgren-0.1.0.dist-info/licenses/LICENSE +201 -0
sundgren/__init__.py
ADDED
sundgren/__main__.py
ADDED
sundgren/cli.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Command line: diff, run, presets, ingest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import sundgren.lang.python # noqa: F401 (lang packs register themselves on import)
|
|
14
|
+
import sundgren.lang.rust # noqa: F401
|
|
15
|
+
from sundgren import __version__
|
|
16
|
+
from sundgren.core.diff import diff
|
|
17
|
+
from sundgren.core.hashing import sha256_file
|
|
18
|
+
from sundgren.core.ingest.lcov import parse as parse_lcov
|
|
19
|
+
from sundgren.core.junit import parse_junit
|
|
20
|
+
from sundgren.core.linemap import LineMap, build_linemaps
|
|
21
|
+
from sundgren.core.model import Outcome, ReportWarning, Snapshot, snapshot_to_dict
|
|
22
|
+
from sundgren.core.normalize import normalize_snapshot
|
|
23
|
+
from sundgren.core.patch import Patch, parse_patch
|
|
24
|
+
from sundgren.core.report.json_report import build_report, dumps
|
|
25
|
+
from sundgren.core.report.text_report import render_text
|
|
26
|
+
from sundgren.run.guards import wrong_tree_paths
|
|
27
|
+
from sundgren.run.presets import Preset, PresetError, all_presets
|
|
28
|
+
from sundgren.run.runner import RunArtifacts, RunFailure, TemplateRunner, adhoc_preset
|
|
29
|
+
from sundgren.run.selectors import Selection, choose, restrict_to_tree
|
|
30
|
+
from sundgren.run.template import default_python
|
|
31
|
+
from sundgren.run.workspace import (
|
|
32
|
+
BadRefError,
|
|
33
|
+
GitError,
|
|
34
|
+
PatchApplyError,
|
|
35
|
+
Workspace,
|
|
36
|
+
is_dirty,
|
|
37
|
+
repo_root,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
EXIT_OK, EXIT_FAILURE, EXIT_USAGE, EXIT_PATCH = 0, 1, 2, 3
|
|
41
|
+
|
|
42
|
+
PYTHON_MARKERS = ("pyproject.toml", "setup.cfg", "pytest.ini")
|
|
43
|
+
RUST_MARKERS = ("Cargo.toml",)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CliError(Exception):
|
|
47
|
+
def __init__(self, message: str, code: int) -> None:
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
self.code = code
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _add_output_args(p: argparse.ArgumentParser) -> None:
|
|
53
|
+
p.add_argument("--format", choices=("json", "text"), default="json")
|
|
54
|
+
p.add_argument("-o", "--output", metavar="OUT", help="write the report here instead of stdout")
|
|
55
|
+
p.add_argument("--full", action="store_true", help="include unchanged lines, functions, tests")
|
|
56
|
+
p.add_argument("--counts", action="store_true", help="fill files[].count_changed")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
60
|
+
parser = argparse.ArgumentParser(prog="sundgren", description=__doc__)
|
|
61
|
+
parser.add_argument("--version", action="version", version=f"sundgren {__version__}")
|
|
62
|
+
sub = parser.add_subparsers(dest="command")
|
|
63
|
+
|
|
64
|
+
d = sub.add_parser("diff", help="diff two existing lcov files")
|
|
65
|
+
d.add_argument("base_lcov", metavar="BASE.lcov")
|
|
66
|
+
d.add_argument("patched_lcov", metavar="PATCHED.lcov")
|
|
67
|
+
d.add_argument("--patch", metavar="FILE", help="patch between the two trees, for line mapping")
|
|
68
|
+
d.add_argument("--base-root", metavar="DIR")
|
|
69
|
+
d.add_argument("--patched-root", metavar="DIR")
|
|
70
|
+
d.add_argument("--strip-prefix", metavar="PREFIX", action="extend", nargs="+", default=[])
|
|
71
|
+
d.add_argument("--base-junit", metavar="FILE")
|
|
72
|
+
d.add_argument("--patched-junit", metavar="FILE")
|
|
73
|
+
_add_output_args(d)
|
|
74
|
+
|
|
75
|
+
r = sub.add_parser("run", help="run coverage on base and patched trees, then diff")
|
|
76
|
+
r.add_argument("--patch", metavar="FILE", required=True)
|
|
77
|
+
how = r.add_mutually_exclusive_group()
|
|
78
|
+
how.add_argument("--preset", metavar="NAME")
|
|
79
|
+
how.add_argument("--cmd", metavar="CMD", action="append", help="ad-hoc command (repeatable)")
|
|
80
|
+
r.add_argument("--lcov", metavar="TEMPLATE", help="lcov output path template (with --cmd)")
|
|
81
|
+
r.add_argument("--junit", metavar="TEMPLATE", help="JUnit output path template (with --cmd)")
|
|
82
|
+
r.add_argument("--shell", action="store_true", help="run --cmd via /bin/sh -c")
|
|
83
|
+
r.add_argument("--repo", metavar="DIR", default=".")
|
|
84
|
+
r.add_argument("--base", metavar="REF", default="HEAD")
|
|
85
|
+
r.add_argument("--tests", metavar="T", action="extend", nargs="+", default=[])
|
|
86
|
+
r.add_argument("--python", metavar="PATH")
|
|
87
|
+
r.add_argument("--env", metavar="K=V", action="append", default=[])
|
|
88
|
+
r.add_argument("--repeat", metavar="N", type=int, default=1)
|
|
89
|
+
r.add_argument("--workdir", metavar="DIR")
|
|
90
|
+
r.add_argument("--keep-worktrees", action="store_true")
|
|
91
|
+
_add_output_args(r)
|
|
92
|
+
|
|
93
|
+
pr = sub.add_parser("presets", help="list built-in and user presets")
|
|
94
|
+
pr.add_argument("--repo", metavar="DIR", default=".")
|
|
95
|
+
|
|
96
|
+
i = sub.add_parser("ingest", help="debug: print a normalized lcov Snapshot as JSON")
|
|
97
|
+
i.add_argument("lcov", metavar="FILE.lcov")
|
|
98
|
+
i.add_argument("--root", metavar="DIR")
|
|
99
|
+
return parser
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
103
|
+
parser = build_parser()
|
|
104
|
+
args = parser.parse_args(argv)
|
|
105
|
+
handlers = {"diff": _cmd_diff, "run": _cmd_run, "presets": _cmd_presets, "ingest": _cmd_ingest}
|
|
106
|
+
if args.command is None:
|
|
107
|
+
parser.print_help(sys.stderr)
|
|
108
|
+
return EXIT_USAGE
|
|
109
|
+
try:
|
|
110
|
+
return handlers[args.command](args)
|
|
111
|
+
except CliError as exc:
|
|
112
|
+
print(f"sundgren: {exc}", file=sys.stderr)
|
|
113
|
+
return exc.code
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --- shared helpers ---------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_text(path: Path, what: str) -> str:
|
|
120
|
+
try:
|
|
121
|
+
return path.read_text(encoding="utf-8")
|
|
122
|
+
except OSError as exc:
|
|
123
|
+
raise CliError(f"cannot read {what} {path}: {exc.strerror}", EXIT_USAGE) from None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _load_snapshot(path: Path, roots: Sequence[str]) -> Snapshot:
|
|
127
|
+
try:
|
|
128
|
+
raw = parse_lcov(_read_text(path, "lcov"))
|
|
129
|
+
except ValueError as exc:
|
|
130
|
+
raise CliError(f"{path}: {exc}", EXIT_FAILURE) from None
|
|
131
|
+
return normalize_snapshot(raw, roots)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_junit(path: Path | None, warnings: list[ReportWarning]) -> dict[str, Outcome] | None:
|
|
135
|
+
if path is None:
|
|
136
|
+
return None
|
|
137
|
+
try:
|
|
138
|
+
return parse_junit(_read_text(path, "JUnit file"))
|
|
139
|
+
except ValueError as exc:
|
|
140
|
+
warnings.append(ReportWarning("junit_unreadable", f"{path.name}: {exc}"))
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _load_patch(path: Path) -> tuple[Patch, dict[str, LineMap], list[ReportWarning]]:
|
|
145
|
+
try:
|
|
146
|
+
# Patches are bytes: keep CRs and undecodable path bytes as they are.
|
|
147
|
+
text = path.read_bytes().decode("utf-8", "surrogateescape")
|
|
148
|
+
except OSError as exc:
|
|
149
|
+
raise CliError(f"cannot read patch {path}: {exc.strerror}", EXIT_USAGE) from None
|
|
150
|
+
return _parse_patch(text, str(path))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _parse_patch(text: str, where: str) -> tuple[Patch, dict[str, LineMap], list[ReportWarning]]:
|
|
154
|
+
try:
|
|
155
|
+
patch = parse_patch(text)
|
|
156
|
+
except ValueError as exc:
|
|
157
|
+
raise CliError(f"{where}: {exc}", EXIT_FAILURE) from None
|
|
158
|
+
warnings = [ReportWarning("patch", w) for w in patch.warnings]
|
|
159
|
+
return patch, build_linemaps(patch), warnings
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _patch_meta(arg: str, path: Path, patch: Patch) -> dict[str, Any]:
|
|
163
|
+
return {"path": arg, "sha256": sha256_file(path), "files": list(patch.touched_paths())}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _meta(**fields: Any) -> dict[str, Any]:
|
|
167
|
+
meta: dict[str, Any] = {
|
|
168
|
+
"sundgren_version": __version__,
|
|
169
|
+
"created_utc": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
170
|
+
"mode": None,
|
|
171
|
+
"base_ref": None,
|
|
172
|
+
"patch": None,
|
|
173
|
+
"preset": None,
|
|
174
|
+
"commands": None,
|
|
175
|
+
"selection": None,
|
|
176
|
+
"repeat": None,
|
|
177
|
+
"inputs": None,
|
|
178
|
+
}
|
|
179
|
+
meta.update(fields)
|
|
180
|
+
return meta
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _emit(report: Mapping[str, Any], args: argparse.Namespace, hints: Sequence[str] = ()) -> None:
|
|
184
|
+
text = dumps(report) if args.format == "json" else render_text(report, hints)
|
|
185
|
+
if args.output:
|
|
186
|
+
Path(args.output).write_text(text, encoding="utf-8")
|
|
187
|
+
else:
|
|
188
|
+
sys.stdout.write(text)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --- diff -------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _cmd_diff(args: argparse.Namespace) -> int:
|
|
195
|
+
base_root = Path(args.base_root).resolve() if args.base_root else None
|
|
196
|
+
patched_root = Path(args.patched_root).resolve() if args.patched_root else None
|
|
197
|
+
base_path, patched_path = Path(args.base_lcov), Path(args.patched_lcov)
|
|
198
|
+
base = _load_snapshot(base_path, [*_roots(base_root), *args.strip_prefix])
|
|
199
|
+
patched = _load_snapshot(patched_path, [*_roots(patched_root), *args.strip_prefix])
|
|
200
|
+
|
|
201
|
+
warnings: list[ReportWarning] = []
|
|
202
|
+
linemaps: dict[str, LineMap] = {}
|
|
203
|
+
patch_meta = None
|
|
204
|
+
if args.patch:
|
|
205
|
+
patch, linemaps, patch_warnings = _load_patch(Path(args.patch))
|
|
206
|
+
warnings += patch_warnings
|
|
207
|
+
patch_meta = _patch_meta(args.patch, Path(args.patch), patch)
|
|
208
|
+
base_junit = _load_junit(Path(args.base_junit) if args.base_junit else None, warnings)
|
|
209
|
+
patched_junit = _load_junit(Path(args.patched_junit) if args.patched_junit else None, warnings)
|
|
210
|
+
|
|
211
|
+
delta = diff(
|
|
212
|
+
[base],
|
|
213
|
+
[patched],
|
|
214
|
+
linemaps,
|
|
215
|
+
base_root=base_root,
|
|
216
|
+
patched_root=patched_root,
|
|
217
|
+
base_outcomes=base_junit,
|
|
218
|
+
patched_outcomes=patched_junit,
|
|
219
|
+
)
|
|
220
|
+
meta = _meta(
|
|
221
|
+
mode="diff",
|
|
222
|
+
patch=patch_meta,
|
|
223
|
+
inputs={
|
|
224
|
+
"base_lcov_sha256": [sha256_file(base_path)],
|
|
225
|
+
"patched_lcov_sha256": [sha256_file(patched_path)],
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
report = build_report(delta, meta, full=args.full, counts=args.counts, extra_warnings=warnings)
|
|
229
|
+
_emit(report, args)
|
|
230
|
+
return EXIT_OK
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _roots(root: Path | None) -> list[str]:
|
|
234
|
+
return [str(root)] if root is not None else []
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# --- run --------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
241
|
+
if args.repeat < 1:
|
|
242
|
+
raise CliError("--repeat must be >= 1", EXIT_USAGE)
|
|
243
|
+
if not args.cmd and (args.lcov or args.junit or args.shell):
|
|
244
|
+
raise CliError("--lcov, --junit and --shell only apply with --cmd", EXIT_USAGE)
|
|
245
|
+
patch_path = Path(args.patch).resolve()
|
|
246
|
+
if not patch_path.is_file():
|
|
247
|
+
raise CliError(f"patch not found: {args.patch}", EXIT_USAGE)
|
|
248
|
+
try:
|
|
249
|
+
repo = repo_root(Path(args.repo))
|
|
250
|
+
except (GitError, OSError) as exc:
|
|
251
|
+
raise CliError(f"not a git repository: {args.repo} ({exc})", EXIT_USAGE) from None
|
|
252
|
+
|
|
253
|
+
preset = _resolve_preset(args, repo)
|
|
254
|
+
env = _parse_env(args.env)
|
|
255
|
+
python = args.python or default_python()
|
|
256
|
+
warnings: list[ReportWarning] = []
|
|
257
|
+
runner = TemplateRunner(preset, python=python, env_overrides=env)
|
|
258
|
+
workdir = Path(args.workdir).resolve() if args.workdir else None
|
|
259
|
+
try:
|
|
260
|
+
if is_dirty(repo):
|
|
261
|
+
warnings.append(
|
|
262
|
+
ReportWarning(
|
|
263
|
+
"dirty_tree", "working tree has uncommitted changes; the base is the ref"
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
with Workspace(
|
|
267
|
+
repo, args.base, patch_path, workdir=workdir, keep=args.keep_worktrees
|
|
268
|
+
) as ws:
|
|
269
|
+
# Use what git applied, not the user's hunk headers or path prefixes.
|
|
270
|
+
patch, linemaps, patch_warnings = _parse_patch(ws.applied_patch, "applied patch")
|
|
271
|
+
warnings += patch_warnings
|
|
272
|
+
if ws.offset_paths:
|
|
273
|
+
warnings.append(
|
|
274
|
+
ReportWarning(
|
|
275
|
+
"patch_offset",
|
|
276
|
+
f"patch applied at an offset in {', '.join(ws.offset_paths)}; "
|
|
277
|
+
"line maps use the applied diff",
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
try:
|
|
281
|
+
selection = choose(tuple(args.tests) or None, preset.selector, patch, ws.patched)
|
|
282
|
+
except KeyError as exc:
|
|
283
|
+
raise CliError(str(exc.args[0]), EXIT_FAILURE) from None
|
|
284
|
+
out = ws.workdir / "out"
|
|
285
|
+
base_snaps, base_junit, base_sha = _run_side(
|
|
286
|
+
"base",
|
|
287
|
+
runner,
|
|
288
|
+
preset,
|
|
289
|
+
selection,
|
|
290
|
+
ws.base,
|
|
291
|
+
out / "base",
|
|
292
|
+
args.repeat,
|
|
293
|
+
repo,
|
|
294
|
+
warnings,
|
|
295
|
+
)
|
|
296
|
+
patched_snaps, patched_junit, patched_sha = _run_side(
|
|
297
|
+
"patched",
|
|
298
|
+
runner,
|
|
299
|
+
preset,
|
|
300
|
+
selection,
|
|
301
|
+
ws.patched,
|
|
302
|
+
out / "patched",
|
|
303
|
+
args.repeat,
|
|
304
|
+
repo,
|
|
305
|
+
warnings,
|
|
306
|
+
)
|
|
307
|
+
delta = diff(
|
|
308
|
+
base_snaps,
|
|
309
|
+
patched_snaps,
|
|
310
|
+
linemaps,
|
|
311
|
+
base_root=ws.base,
|
|
312
|
+
patched_root=ws.patched,
|
|
313
|
+
base_outcomes=base_junit,
|
|
314
|
+
patched_outcomes=patched_junit,
|
|
315
|
+
)
|
|
316
|
+
base_ref = ws.base_sha
|
|
317
|
+
patched_selection = restrict_to_tree(selection, ws.patched) or selection
|
|
318
|
+
except PatchApplyError as exc:
|
|
319
|
+
raise CliError(f"patch does not apply:\n{exc}", EXIT_PATCH) from None
|
|
320
|
+
except BadRefError as exc:
|
|
321
|
+
raise CliError(str(exc), EXIT_USAGE) from None
|
|
322
|
+
except GitError as exc:
|
|
323
|
+
raise CliError(f"git: {exc}", EXIT_FAILURE) from None
|
|
324
|
+
except RunFailure as exc:
|
|
325
|
+
raise CliError(f"{exc}\n--- log tail ---\n{exc.log_tail}", EXIT_FAILURE) from None
|
|
326
|
+
|
|
327
|
+
meta = _meta(
|
|
328
|
+
mode="run",
|
|
329
|
+
base_ref=base_ref,
|
|
330
|
+
patch=_patch_meta(args.patch, patch_path, patch),
|
|
331
|
+
preset=preset.name,
|
|
332
|
+
commands=runner.display_commands(patched_selection),
|
|
333
|
+
selection={
|
|
334
|
+
"tests": list(patched_selection.tests),
|
|
335
|
+
"run_all": patched_selection.run_all,
|
|
336
|
+
"reason": patched_selection.reason,
|
|
337
|
+
},
|
|
338
|
+
repeat=args.repeat,
|
|
339
|
+
inputs={"base_lcov_sha256": base_sha, "patched_lcov_sha256": patched_sha},
|
|
340
|
+
)
|
|
341
|
+
report = build_report(delta, meta, full=args.full, counts=args.counts, extra_warnings=warnings)
|
|
342
|
+
_emit(report, args, preset.hints)
|
|
343
|
+
return EXIT_OK
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _run_side(
|
|
347
|
+
side: str,
|
|
348
|
+
runner: TemplateRunner,
|
|
349
|
+
preset: Preset,
|
|
350
|
+
selection: Selection,
|
|
351
|
+
worktree: Path,
|
|
352
|
+
out: Path,
|
|
353
|
+
repeat: int,
|
|
354
|
+
repo: Path,
|
|
355
|
+
warnings: list[ReportWarning],
|
|
356
|
+
) -> tuple[list[Snapshot], dict[str, Outcome] | None, list[str]]:
|
|
357
|
+
"""Run one side; returns its snapshots, first-run outcomes and lcov hashes."""
|
|
358
|
+
side_selection: Selection | None = selection
|
|
359
|
+
if preset.tests_style == "args":
|
|
360
|
+
side_selection = restrict_to_tree(selection, worktree)
|
|
361
|
+
if side_selection is None:
|
|
362
|
+
# Every selected test is new in the patch: nothing ran on this side.
|
|
363
|
+
warnings.append(
|
|
364
|
+
ReportWarning(
|
|
365
|
+
"nothing_to_run",
|
|
366
|
+
f"no selected tests exist on the {side} side; its coverage is treated as empty",
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
return [Snapshot(files={}, test_names=())], {}, []
|
|
370
|
+
|
|
371
|
+
runs: list[RunArtifacts] = runner.run(worktree, side_selection, out, repeat)
|
|
372
|
+
snapshots: list[Snapshot] = []
|
|
373
|
+
hashes: list[str] = []
|
|
374
|
+
for art in runs:
|
|
375
|
+
try:
|
|
376
|
+
raw = parse_lcov(art.lcov.read_text(encoding="utf-8"))
|
|
377
|
+
except ValueError as exc:
|
|
378
|
+
raise CliError(f"{side}: bad lcov output: {exc}", EXIT_FAILURE) from None
|
|
379
|
+
wrong = wrong_tree_paths((fc.path for fc in raw.files.values()), worktree, repo)
|
|
380
|
+
if wrong:
|
|
381
|
+
raise CliError(_wrong_tree_message(side, wrong, preset), EXIT_FAILURE)
|
|
382
|
+
snapshots.append(normalize_snapshot(raw, [str(worktree)]))
|
|
383
|
+
hashes.append(sha256_file(art.lcov))
|
|
384
|
+
junit = _load_junit(runs[0].junit, warnings)
|
|
385
|
+
return snapshots, junit, hashes
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _wrong_tree_message(side: str, paths: Sequence[str], preset: Preset) -> str:
|
|
389
|
+
lines = [
|
|
390
|
+
f"the {side} run measured your main checkout instead of its worktree "
|
|
391
|
+
"(editable install or stale build?):",
|
|
392
|
+
*(f" {p}" for p in paths[:20]),
|
|
393
|
+
]
|
|
394
|
+
if len(paths) > 20:
|
|
395
|
+
lines.append(f" ... and {len(paths) - 20} more")
|
|
396
|
+
if preset.hints:
|
|
397
|
+
lines.append("hints:")
|
|
398
|
+
lines.extend(f" - {h}" for h in preset.hints)
|
|
399
|
+
return "\n".join(lines)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _resolve_preset(args: argparse.Namespace, repo: Path) -> Preset:
|
|
403
|
+
if args.cmd:
|
|
404
|
+
if not args.lcov:
|
|
405
|
+
raise CliError("--cmd needs --lcov", EXIT_USAGE)
|
|
406
|
+
return adhoc_preset(tuple(args.cmd), args.lcov, args.junit, shell=args.shell)
|
|
407
|
+
try:
|
|
408
|
+
presets = all_presets(repo)
|
|
409
|
+
except PresetError as exc:
|
|
410
|
+
raise CliError(str(exc), EXIT_USAGE) from None
|
|
411
|
+
name = args.preset or _default_preset_name(repo)
|
|
412
|
+
if name not in presets:
|
|
413
|
+
known = ", ".join(sorted(presets))
|
|
414
|
+
raise CliError(f"unknown preset {name!r} (known: {known})", EXIT_USAGE)
|
|
415
|
+
return presets[name]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _default_preset_name(repo: Path) -> str:
|
|
419
|
+
python = any((repo / m).exists() for m in PYTHON_MARKERS)
|
|
420
|
+
rust = any((repo / m).exists() for m in RUST_MARKERS)
|
|
421
|
+
if python and not rust:
|
|
422
|
+
return "pytest"
|
|
423
|
+
if rust and not python:
|
|
424
|
+
return "cargo-llvm-cov"
|
|
425
|
+
raise CliError("can't pick a default preset here; pass --preset or --cmd", EXIT_USAGE)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _parse_env(items: Sequence[str]) -> dict[str, str]:
|
|
429
|
+
env: dict[str, str] = {}
|
|
430
|
+
for item in items:
|
|
431
|
+
key, sep, value = item.partition("=")
|
|
432
|
+
if not sep or not key:
|
|
433
|
+
raise CliError(f"--env expects K=V, got {item!r}", EXIT_USAGE)
|
|
434
|
+
env[key] = value
|
|
435
|
+
return env
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# --- presets / ingest -------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _cmd_presets(args: argparse.Namespace) -> int:
|
|
442
|
+
try:
|
|
443
|
+
presets = all_presets(Path(args.repo))
|
|
444
|
+
except PresetError as exc:
|
|
445
|
+
raise CliError(str(exc), EXIT_USAGE) from None
|
|
446
|
+
for name in sorted(presets):
|
|
447
|
+
p = presets[name]
|
|
448
|
+
print(f"{name} [{p.source}] {p.description}")
|
|
449
|
+
print(f" tests: {p.tests_style}, selector: {p.selector or '-'}")
|
|
450
|
+
for cmd in p.commands:
|
|
451
|
+
print(f" $ {cmd}")
|
|
452
|
+
return EXIT_OK
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _cmd_ingest(args: argparse.Namespace) -> int:
|
|
456
|
+
roots = _roots(Path(args.root).resolve() if args.root else None)
|
|
457
|
+
snapshot = _load_snapshot(Path(args.lcov), roots)
|
|
458
|
+
sys.stdout.write(json.dumps(snapshot_to_dict(snapshot), indent=2, sort_keys=True) + "\n")
|
|
459
|
+
return EXIT_OK
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Language-agnostic core: lcov, unified diffs, JUnit XML, the output schema."""
|