leyline-code 0.8.1__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.
- leyline/__init__.py +3 -0
- leyline/adapters/__init__.py +19 -0
- leyline/adapters/csharp.py +1026 -0
- leyline/adapters/generic.py +844 -0
- leyline/adapters/python.py +929 -0
- leyline/adapters/typescript.py +1512 -0
- leyline/affected.py +509 -0
- leyline/agent_skills.py +311 -0
- leyline/change.py +500 -0
- leyline/channels.py +1782 -0
- leyline/cli.py +1029 -0
- leyline/cluster.py +149 -0
- leyline/context.py +531 -0
- leyline/coupling.py +485 -0
- leyline/coverage.py +376 -0
- leyline/diagrams.py +680 -0
- leyline/diff.py +1078 -0
- leyline/drift.py +747 -0
- leyline/exact.py +410 -0
- leyline/explain.py +973 -0
- leyline/export.py +673 -0
- leyline/fncoupling.py +328 -0
- leyline/formats.py +923 -0
- leyline/grade.py +207 -0
- leyline/incremental.py +931 -0
- leyline/indexer.py +3733 -0
- leyline/learnings.py +743 -0
- leyline/loop.py +584 -0
- leyline/model.py +142 -0
- leyline/outline.py +1066 -0
- leyline/packing.py +86 -0
- leyline/patterns.py +478 -0
- leyline/pr.py +1249 -0
- leyline/props.py +255 -0
- leyline/query.py +683 -0
- leyline/quick.py +853 -0
- leyline/related.py +266 -0
- leyline/removal.py +231 -0
- leyline/renames.py +231 -0
- leyline/rereview.py +375 -0
- leyline/roslyn_refs/Program.cs +179 -0
- leyline/roslyn_refs/RoslynRefs.csproj +16 -0
- leyline/rules.py +171 -0
- leyline/schema.sql +178 -0
- leyline/scip_pb2.py +97 -0
- leyline/server.py +1230 -0
- leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
- leyline/skills/leyline-ask/SKILL.md +57 -0
- leyline/skills/leyline-change-impact/SKILL.md +79 -0
- leyline/skills/leyline-explain-flow/SKILL.md +70 -0
- leyline/skills/leyline-explore-module/SKILL.md +55 -0
- leyline/skills/leyline-pr-review/SKILL.md +77 -0
- leyline/skills/leyline-quick-change/SKILL.md +63 -0
- leyline/skills/leyline-spec/SKILL.md +179 -0
- leyline/skills/leyline-tour/SKILL.md +46 -0
- leyline/spec.py +1819 -0
- leyline/store.py +560 -0
- leyline/tours.py +352 -0
- leyline/verdicts.py +390 -0
- leyline/viewer/viewer.html +1622 -0
- leyline_code-0.8.1.dist-info/METADATA +1325 -0
- leyline_code-0.8.1.dist-info/RECORD +65 -0
- leyline_code-0.8.1.dist-info/WHEEL +4 -0
- leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
- leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
leyline/coverage.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Measured coverage: which functions ran, and under which test.
|
|
2
|
+
|
|
3
|
+
Static flows say what a test can reach. A coverage file says what it did reach. Importing one lets
|
|
4
|
+
the map tell the two apart: a step on a test's static path that never ran sits behind a branch the
|
|
5
|
+
test does not take, and a function that ran without being on the path was reached through a link
|
|
6
|
+
the indexer could not see.
|
|
7
|
+
|
|
8
|
+
Formats read:
|
|
9
|
+
- coverage.py's data file (`.coverage`). Run the tests with `--cov-context=test` (pytest-cov) or
|
|
10
|
+
`dynamic_context = test_function` and each function is tied to the tests that ran it.
|
|
11
|
+
- Cobertura XML (coverlet, `coverage xml`, many others). No per-test detail: a function ran or did not.
|
|
12
|
+
- Istanbul's `coverage-final.json` (vitest `--coverage.reporter=json`, jest `--coverageReporters=json`, c8, nyc). It
|
|
13
|
+
is one file per run, not per test. Run one test file at a time and import each result with `test` naming the file,
|
|
14
|
+
and every function is tied to the test file that ran it (not to the single test inside it).
|
|
15
|
+
- lcov (`lcov.info`: c8, vitest and jest `--coverage.reporter=lcov`, `go tool`/llvm-cov exports, genhtml's input). Its
|
|
16
|
+
`SF:`, `DA:`, `FN:` and `FNDA:` records give the lines and functions that ran; a `TN:` before them names the test,
|
|
17
|
+
and its records are tied to that test (by pytest node id or by name), else the file is one run like Istanbul's.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import datetime
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import sqlite3
|
|
26
|
+
import xml.etree.ElementTree as ET
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from . import store
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _functions(con):
|
|
35
|
+
"""(repo, path) -> [(first body line, last line, node id)] for every function on the map."""
|
|
36
|
+
out = defaultdict(list)
|
|
37
|
+
for r in con.execute("SELECT id, repo_id, path, span_start, span_end, attrs FROM nodes"
|
|
38
|
+
" WHERE kind IN ('callable', 'test') AND path IS NOT NULL"):
|
|
39
|
+
if not r["span_start"]:
|
|
40
|
+
continue
|
|
41
|
+
a = json.loads(r["attrs"] or "{}")
|
|
42
|
+
end = r["span_end"] or r["span_start"]
|
|
43
|
+
# The line that declares a function runs when its file loads or its parent runs. Only the body counts.
|
|
44
|
+
first = a.get("body_line") or (r["span_start"] + 1 if end > r["span_start"] else r["span_start"])
|
|
45
|
+
out[(r["repo_id"], r["path"])].append((min(first, end), end, r["id"]))
|
|
46
|
+
return out
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _owners(funcs, lines):
|
|
50
|
+
"""For each executed line, the innermost function whose body holds it. Returns node id -> line count."""
|
|
51
|
+
hit = defaultdict(int)
|
|
52
|
+
for line in lines:
|
|
53
|
+
best = None
|
|
54
|
+
for first, end, node in funcs:
|
|
55
|
+
if first <= line <= end and (best is None or first > best[0]):
|
|
56
|
+
best = (first, node)
|
|
57
|
+
if best:
|
|
58
|
+
hit[best[1]] += 1
|
|
59
|
+
return hit
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _relative(path: str, roots: dict, known: set) -> Optional[tuple]:
|
|
63
|
+
"""A measured file's (repo, path) on the map. `roots` is repo id -> the directory it was indexed from."""
|
|
64
|
+
p = path.replace("\\", "/")
|
|
65
|
+
for repo, root in roots.items():
|
|
66
|
+
root = (root or "").replace("\\", "/").rstrip("/") # a Windows root is stored with backslashes
|
|
67
|
+
if root and p.startswith(root + "/") and (repo, p[len(root) + 1:]) in known:
|
|
68
|
+
return repo, p[len(root) + 1:]
|
|
69
|
+
exact = sorted(k for k in known if k[1] == p)
|
|
70
|
+
if exact:
|
|
71
|
+
return exact[0]
|
|
72
|
+
hits = [k for k in known if p.endswith("/" + k[1]) or k[1].endswith("/" + p)] # a different checkout location
|
|
73
|
+
return max(hits, key=lambda k: (len(k[1]), k)) if hits else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _numbits(blob: bytes) -> list[int]:
|
|
77
|
+
return [i * 8 + b for i, byte in enumerate(blob) for b in range(8) if byte & (1 << b)]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _test_node(con, context: str, cache: dict, home: str = "") -> Optional[str]:
|
|
81
|
+
"""`tests/test_x.py::TestC::test_m[param]|run` -> the test function's node id. In a workspace, a test
|
|
82
|
+
in `home` (the repo whose code was measured) wins over one at the same path in another repo."""
|
|
83
|
+
name = context.split("|", 1)[0]
|
|
84
|
+
name = re.sub(r"\[.*\]$", "", name)
|
|
85
|
+
if name in cache:
|
|
86
|
+
return cache[name]
|
|
87
|
+
node = None
|
|
88
|
+
order, extra = (" ORDER BY repo_id != ?", (home,)) if home else ("", ())
|
|
89
|
+
if "::" in name:
|
|
90
|
+
path, *parts = name.split("::")
|
|
91
|
+
row = con.execute("SELECT id FROM nodes WHERE kind IN ('callable', 'test') AND path = ? AND id LIKE ?" + order,
|
|
92
|
+
(path, "%." + ".".join(parts), *extra)).fetchone()
|
|
93
|
+
if row is None:
|
|
94
|
+
row = con.execute("SELECT id FROM nodes WHERE kind IN ('callable', 'test') AND path LIKE ? AND id LIKE ?" + order,
|
|
95
|
+
("%" + path, "%." + ".".join(parts), *extra)).fetchone()
|
|
96
|
+
node = row[0] if row else None
|
|
97
|
+
elif name:
|
|
98
|
+
row = con.execute("SELECT id FROM nodes WHERE kind IN ('callable', 'test') AND (id LIKE ? OR name = ?)" + order,
|
|
99
|
+
("%" + name, name, *extra)).fetchone() # dynamic_context = test_function gives a dotted name
|
|
100
|
+
node = row[0] if row else None
|
|
101
|
+
cache[name] = node
|
|
102
|
+
return node
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _test_file_node(con, test: str, roots: dict) -> Optional[str]:
|
|
106
|
+
"""The map's node for a test file named on the command line (relative to anywhere, or absolute)."""
|
|
107
|
+
files = {(r["repo_id"], r["path"]): r["id"] for r in con.execute(
|
|
108
|
+
"SELECT id, repo_id, path FROM nodes WHERE kind = 'file' AND path IS NOT NULL")}
|
|
109
|
+
whole = str(Path(test).resolve()) if Path(test).exists() else test
|
|
110
|
+
rel = _relative(whole, roots, set(files)) or _relative(test.replace("\\", "/").lstrip("./"), roots, set(files))
|
|
111
|
+
return files.get(rel) if rel else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _istanbul(data) -> bool:
|
|
115
|
+
return isinstance(data, dict) and bool(data) and all(
|
|
116
|
+
isinstance(v, dict) and "statementMap" in v and "s" in v for v in list(data.values())[:5])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def import_file(con, path: str | Path, run: str = "default", test: Optional[str] = None) -> dict:
|
|
120
|
+
"""Read a coverage file into the store, replacing any earlier import under the same run name. `test` names the
|
|
121
|
+
test file a run-wide report (Istanbul, Cobertura) came from when only that file ran; such an import is kept
|
|
122
|
+
under its own run name (`test:<file>`) unless `run` names one."""
|
|
123
|
+
path = Path(path)
|
|
124
|
+
if not path.is_file():
|
|
125
|
+
return {"error": f"no file at {path}"}
|
|
126
|
+
head = path.read_bytes()[:64]
|
|
127
|
+
if test and run == "default":
|
|
128
|
+
run = "test:" + test
|
|
129
|
+
roots = {k: str(v) for k, v in store.roots(con).items()}
|
|
130
|
+
funcs = _functions(con)
|
|
131
|
+
known = set(funcs)
|
|
132
|
+
rows: list[tuple] = []
|
|
133
|
+
tests_seen, tests_matched, files_matched, files_unknown = set(), set(), 0, 0
|
|
134
|
+
if head.startswith(b"SQLite format 3"):
|
|
135
|
+
fmt = "coverage.py"
|
|
136
|
+
src = sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True) # quoted: `#`, `?`, `%` in a folder's name
|
|
137
|
+
contexts = dict(src.execute("SELECT id, context FROM context"))
|
|
138
|
+
cache: dict = {}
|
|
139
|
+
per: dict[tuple, set] = defaultdict(set) # (file path, context id) -> lines
|
|
140
|
+
files = dict(src.execute("SELECT id, path FROM file"))
|
|
141
|
+
rel = {i: _relative(p, roots, known) for i, p in files.items()}
|
|
142
|
+
files_matched = sum(1 for v in rel.values() if v)
|
|
143
|
+
files_unknown = sum(1 for v in rel.values() if not v)
|
|
144
|
+
home = max(roots, key=lambda r: sum(1 for v in rel.values() if v and v[0] == r)) if len(roots) > 1 else ""
|
|
145
|
+
for fid, cid, a, b in src.execute("SELECT file_id, context_id, fromno, tono FROM arc"):
|
|
146
|
+
if rel.get(fid):
|
|
147
|
+
per[(rel[fid], cid)].update(x for x in (a, b) if x > 0)
|
|
148
|
+
for fid, cid, bits in src.execute("SELECT file_id, context_id, numbits FROM line_bits"):
|
|
149
|
+
if rel.get(fid):
|
|
150
|
+
per[(rel[fid], cid)].update(_numbits(bits))
|
|
151
|
+
src.close()
|
|
152
|
+
merged: dict[tuple, int] = defaultdict(int) # (test name, node) -> lines, with setup, run and teardown together
|
|
153
|
+
for (file, cid), lines in per.items():
|
|
154
|
+
ctx = contexts.get(cid, "")
|
|
155
|
+
test = re.sub(r"\[.*\]$", "", ctx.split("|", 1)[0])
|
|
156
|
+
for node, n in _owners(funcs[file], lines).items():
|
|
157
|
+
merged[(test, node)] += n
|
|
158
|
+
for (test, node), n in merged.items():
|
|
159
|
+
tid = _test_node(con, test, cache, home) if test else None
|
|
160
|
+
if test:
|
|
161
|
+
tests_seen.add(test)
|
|
162
|
+
if tid:
|
|
163
|
+
tests_matched.add(test)
|
|
164
|
+
rows.append((run, test, tid, node, n))
|
|
165
|
+
elif b"<?xml" in head or b"<coverage" in head:
|
|
166
|
+
fmt = "cobertura"
|
|
167
|
+
tree = ET.parse(path)
|
|
168
|
+
sources = [s.text for s in tree.getroot().iter("source") if s.text]
|
|
169
|
+
per_file: dict[str, set] = defaultdict(set)
|
|
170
|
+
for cls in tree.getroot().iter("class"):
|
|
171
|
+
name = cls.get("filename") or ""
|
|
172
|
+
rel = _relative(name, roots, known) or next(
|
|
173
|
+
(r for s in sources for r in [_relative(s.rstrip("/") + "/" + name, roots, known)] if r), None)
|
|
174
|
+
if not rel:
|
|
175
|
+
files_unknown += 1
|
|
176
|
+
continue
|
|
177
|
+
for ln in cls.iter("line"):
|
|
178
|
+
if int(ln.get("hits", "0") or 0) > 0:
|
|
179
|
+
per_file[rel].add(int(ln.get("number")))
|
|
180
|
+
files_matched = len(per_file)
|
|
181
|
+
for file, lines in per_file.items():
|
|
182
|
+
for node, n in _owners(funcs[file], lines).items():
|
|
183
|
+
rows.append((run, "", None, node, n))
|
|
184
|
+
elif head.lstrip().startswith(b"{"):
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
187
|
+
except (ValueError, RecursionError):
|
|
188
|
+
data = None
|
|
189
|
+
if not _istanbul(data):
|
|
190
|
+
return {"error": "a JSON file, but not Istanbul's coverage-final.json (vitest --coverage.reporter=json,"
|
|
191
|
+
" jest --coverageReporters=json). V8's raw output is not read: convert it with c8 or vitest."}
|
|
192
|
+
fmt = "istanbul"
|
|
193
|
+
per_file = defaultdict(set)
|
|
194
|
+
for key, f in data.items():
|
|
195
|
+
rel = _relative(f.get("path") or key, roots, known)
|
|
196
|
+
if not rel:
|
|
197
|
+
files_unknown += 1
|
|
198
|
+
continue
|
|
199
|
+
smap, fmap = f.get("statementMap") or {}, f.get("fnMap") or {}
|
|
200
|
+
# v8's conversion counts a declaration or closing-brace line as run when the file loads: a line inside a
|
|
201
|
+
# function that never ran did not run.
|
|
202
|
+
fns = [((loc.get("start") or {}).get("line") or 0, (loc.get("end") or {}).get("line") or 0, (f.get("f") or {}).get(k, 0))
|
|
203
|
+
for k, m in fmap.items() for loc in [m.get("loc") or m.get("decl") or {}]]
|
|
204
|
+
for sid, count in (f.get("s") or {}).items():
|
|
205
|
+
line = ((smap.get(sid) or {}).get("start") or {}).get("line")
|
|
206
|
+
if not count or not line:
|
|
207
|
+
continue
|
|
208
|
+
inner = [x for x in fns if x[0] <= line <= x[1]]
|
|
209
|
+
if inner and not max(inner, key=lambda x: (x[0], -x[1]))[2]:
|
|
210
|
+
continue
|
|
211
|
+
per_file[rel].add(int(line))
|
|
212
|
+
files_matched = len(per_file)
|
|
213
|
+
for file, lines in per_file.items():
|
|
214
|
+
for node, n in _owners(funcs[file], lines).items():
|
|
215
|
+
rows.append((run, "", None, node, n))
|
|
216
|
+
elif head.lstrip().startswith((b"TN:", b"SF:")):
|
|
217
|
+
fmt = "lcov"
|
|
218
|
+
per_test: dict[tuple, set] = defaultdict(set) # (test name, file) -> lines run
|
|
219
|
+
seen_files: dict[str, Optional[tuple]] = {}
|
|
220
|
+
state = {"test": "", "file": None, "lines": set(), "fns": {}}
|
|
221
|
+
|
|
222
|
+
def close() -> None:
|
|
223
|
+
# as for Istanbul: a line inside a function that never ran did not run (v8's conversion counts a
|
|
224
|
+
# declaration or closing-brace line as run when the file loads); FN gives the end line from lcov 2.0
|
|
225
|
+
rel, fns = state["file"], list(state["fns"].values())
|
|
226
|
+
if rel:
|
|
227
|
+
for line in state["lines"]:
|
|
228
|
+
inner = [x for x in fns if x[1] is not None and x[0] <= line <= x[1]]
|
|
229
|
+
if inner and not max(inner, key=lambda x: (x[0], -x[1]))[2]:
|
|
230
|
+
continue
|
|
231
|
+
per_test[(state["test"], rel)].add(line)
|
|
232
|
+
state.update(file=None, lines=set(), fns={})
|
|
233
|
+
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
234
|
+
line = raw.strip()
|
|
235
|
+
key, _, val = line.partition(":")
|
|
236
|
+
try:
|
|
237
|
+
if key == "TN":
|
|
238
|
+
state["test"] = val.strip()
|
|
239
|
+
elif key == "SF":
|
|
240
|
+
name = val.strip()
|
|
241
|
+
if name not in seen_files:
|
|
242
|
+
seen_files[name] = _relative(name, roots, known)
|
|
243
|
+
state.update(file=seen_files[name], lines=set(), fns={})
|
|
244
|
+
elif key == "FN":
|
|
245
|
+
parts = val.split(",")
|
|
246
|
+
if len(parts) >= 3 and parts[1].strip().isdigit(): # FN:<start>,<end>,<name>
|
|
247
|
+
state["fns"][",".join(parts[2:])] = [int(parts[0]), int(parts[1]), 0]
|
|
248
|
+
elif len(parts) >= 2: # FN:<start>,<name>
|
|
249
|
+
state["fns"][",".join(parts[1:])] = [int(parts[0]), None, 0]
|
|
250
|
+
elif key == "FNDA":
|
|
251
|
+
count, _, name = val.partition(",")
|
|
252
|
+
if name in state["fns"]:
|
|
253
|
+
state["fns"][name][2] = float(count or 0)
|
|
254
|
+
elif key == "DA":
|
|
255
|
+
parts = val.split(",")
|
|
256
|
+
if len(parts) >= 2 and float(parts[1] or 0) > 0:
|
|
257
|
+
state["lines"].add(int(parts[0]))
|
|
258
|
+
elif line == "end_of_record":
|
|
259
|
+
close()
|
|
260
|
+
except ValueError: # a malformed record line: the rest of the file is still read
|
|
261
|
+
continue
|
|
262
|
+
close()
|
|
263
|
+
files_matched = len({v for v in seen_files.values() if v})
|
|
264
|
+
files_unknown = sum(1 for v in seen_files.values() if not v)
|
|
265
|
+
home = max(roots, key=lambda r: sum(1 for v in seen_files.values() if v and v[0] == r)) if len(roots) > 1 else ""
|
|
266
|
+
cache: dict = {}
|
|
267
|
+
merged = defaultdict(int)
|
|
268
|
+
for (name, file), lines in per_test.items():
|
|
269
|
+
for node, n in _owners(funcs[file], lines).items():
|
|
270
|
+
merged[(name, node)] += n
|
|
271
|
+
for (name, node), n in merged.items():
|
|
272
|
+
tid = _test_node(con, name, cache, home) if name else None
|
|
273
|
+
if name:
|
|
274
|
+
tests_seen.add(name)
|
|
275
|
+
if tid:
|
|
276
|
+
tests_matched.add(name)
|
|
277
|
+
rows.append((run, name, tid, node, n))
|
|
278
|
+
else:
|
|
279
|
+
return {"error": "not a coverage.py data file, a Cobertura XML report, Istanbul's coverage-final.json or an"
|
|
280
|
+
" lcov file"}
|
|
281
|
+
if test and fmt != "coverage.py" and not tests_seen: # a run of one test file: everything that ran, ran under it
|
|
282
|
+
tid = _test_file_node(con, test, roots)
|
|
283
|
+
rows = [(r[0], test, tid, r[3], r[4]) for r in rows]
|
|
284
|
+
tests_seen, tests_matched = {test}, ({test} if tid else set())
|
|
285
|
+
head_commit = con.execute("SELECT commit_sha FROM nodes WHERE kind = 'repo' LIMIT 1").fetchone()
|
|
286
|
+
ran = {r[3] for r in rows}
|
|
287
|
+
stats = {"functions_ran": len(ran), "tests": len(tests_seen), "tests_matched_to_the_map": len(tests_matched),
|
|
288
|
+
"files_matched": files_matched, "files_not_on_the_map": files_unknown, "per_test": bool(tests_seen),
|
|
289
|
+
**({"per": "test file"} if test and fmt != "coverage.py" else {})}
|
|
290
|
+
with con:
|
|
291
|
+
con.execute("DELETE FROM covered WHERE run = ?", (run,))
|
|
292
|
+
con.executemany("INSERT INTO covered VALUES (?,?,?,?,?)", rows)
|
|
293
|
+
con.execute("INSERT OR REPLACE INTO coverage_runs VALUES (?,?,?,?,?)",
|
|
294
|
+
(run, fmt, datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"),
|
|
295
|
+
head_commit[0] if head_commit else None, json.dumps(stats)))
|
|
296
|
+
for repo in con.execute("SELECT id FROM nodes WHERE kind = 'repo'").fetchall():
|
|
297
|
+
con.execute("INSERT OR REPLACE INTO extractor_coverage VALUES (?,?,?,?,?,?)",
|
|
298
|
+
(repo[0], "coverage", fmt, "ok", head_commit[0] if head_commit else None, json.dumps(stats)))
|
|
299
|
+
try: # the orientation tour quotes these numbers
|
|
300
|
+
from . import tours
|
|
301
|
+
for repo in con.execute("SELECT id FROM nodes WHERE kind = 'repo'").fetchall():
|
|
302
|
+
tours.generate(con, repo[0])
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
return {"run": run, "format": fmt, **stats}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def has(con) -> bool:
|
|
309
|
+
try:
|
|
310
|
+
return con.execute("SELECT 1 FROM covered LIMIT 1").fetchone() is not None
|
|
311
|
+
except sqlite3.OperationalError:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def ran(con) -> set:
|
|
316
|
+
"""Every function some imported run executed."""
|
|
317
|
+
return {r[0] for r in con.execute("SELECT DISTINCT node_id FROM covered")} if has(con) else set()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def tests_for(con, node_id: str, limit: int = 50) -> list[dict]:
|
|
321
|
+
"""The tests under which a function ran."""
|
|
322
|
+
return [{"test": r["test"], "id": r["test_id"], "lines": r["n"]} for r in con.execute(
|
|
323
|
+
"SELECT test, test_id, SUM(lines) AS n FROM covered WHERE node_id = ? AND test != '' GROUP BY test ORDER BY n DESC LIMIT ?",
|
|
324
|
+
(node_id, limit))]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def summary(con) -> dict:
|
|
328
|
+
if not has(con):
|
|
329
|
+
return {"imported": False, "how": "Run the tests under a coverage tool, then `leyline coverage <file>`. "
|
|
330
|
+
"With pytest: `pytest --cov=<package> --cov-context=test`."}
|
|
331
|
+
module = {r["node_id"]: r["module_id"] for r in con.execute("SELECT node_id, module_id FROM ancestry")}
|
|
332
|
+
names = {r["id"]: r["name"] for r in con.execute("SELECT id, name FROM nodes WHERE kind = 'module'")}
|
|
333
|
+
executed = ran(con)
|
|
334
|
+
measured_files = {r[0] for r in con.execute(
|
|
335
|
+
"SELECT DISTINCT a.file_id FROM covered c JOIN ancestry a ON a.node_id = c.node_id")}
|
|
336
|
+
static = {r[0] for r in con.execute(store.TESTED)}
|
|
337
|
+
by_module = defaultdict(lambda: {"functions": 0, "ran": 0, "on_a_test_path": 0, "path_but_never_ran": 0, "ran_off_every_path": 0})
|
|
338
|
+
for r in con.execute("SELECT n.id, a.file_id FROM nodes n JOIN ancestry a ON a.node_id = n.id WHERE n.kind = 'callable'"):
|
|
339
|
+
if r["file_id"] not in measured_files:
|
|
340
|
+
continue # a file the coverage tool did not watch says nothing either way
|
|
341
|
+
m = by_module[module.get(r["id"])]
|
|
342
|
+
m["functions"] += 1
|
|
343
|
+
m["ran"] += r["id"] in executed
|
|
344
|
+
m["on_a_test_path"] += r["id"] in static
|
|
345
|
+
m["path_but_never_ran"] += r["id"] in static and r["id"] not in executed
|
|
346
|
+
m["ran_off_every_path"] += r["id"] in executed and r["id"] not in static
|
|
347
|
+
head = con.execute("SELECT commit_sha FROM nodes WHERE kind = 'repo' LIMIT 1").fetchone()
|
|
348
|
+
runs = [{"run": r["run"], "format": r["format"], "created": r["created"], **json.loads(r["stats"] or "{}"),
|
|
349
|
+
"stale": bool(head and r["commit_sha"] and r["commit_sha"] != head[0])}
|
|
350
|
+
for r in con.execute("SELECT * FROM coverage_runs ORDER BY created DESC")]
|
|
351
|
+
return {"imported": True, "runs": runs,
|
|
352
|
+
"modules": [{"module": names.get(m, m), **v} for m, v in sorted(by_module.items(), key=lambda kv: -kv[1]["functions"])],
|
|
353
|
+
"note": "Only files the coverage tool watched are counted. 'Path but never ran' is code a test can reach on paper "
|
|
354
|
+
"and did not; 'ran off every path' is code that ran through a link the map does not have."}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def compare_flow(con, flow_id: str) -> dict:
|
|
358
|
+
"""One test's static path against what ran when it was measured."""
|
|
359
|
+
f = con.execute("SELECT id, name, entry_id FROM flows WHERE id = ?", (flow_id,)).fetchone()
|
|
360
|
+
if f is None:
|
|
361
|
+
return {"error": f"No flow {flow_id!r}."}
|
|
362
|
+
executed = {r[0] for r in con.execute("SELECT node_id FROM covered WHERE test_id = ?", (f["entry_id"],))}
|
|
363
|
+
if not executed:
|
|
364
|
+
return {"flow": flow_id, "measured": False}
|
|
365
|
+
watched = {r[0] for r in con.execute("SELECT DISTINCT a.file_id FROM covered c JOIN ancestry a ON a.node_id = c.node_id")}
|
|
366
|
+
file_of = {r["node_id"]: r["file_id"] for r in con.execute("SELECT node_id, file_id FROM ancestry")}
|
|
367
|
+
names = {r["id"]: r["name"] for r in con.execute("SELECT id, name FROM nodes")}
|
|
368
|
+
steps = [r[0] for r in con.execute("SELECT callable_id FROM flow_steps WHERE flow_id = ? ORDER BY seq", (flow_id,))]
|
|
369
|
+
in_scope = [s for s in steps if file_of.get(s) in watched]
|
|
370
|
+
never = [s for s in in_scope if s not in executed]
|
|
371
|
+
extra = sorted(i for i in executed - set(steps) if not names.get(i, "").startswith("<")) # not module bodies
|
|
372
|
+
return {"flow": flow_id, "test": f["name"], "measured": True, "steps_watched": len(in_scope),
|
|
373
|
+
"ran": len(in_scope) - len(never),
|
|
374
|
+
"on_path_but_did_not_run": [{"id": i, "name": names.get(i, i)} for i in never[:60]],
|
|
375
|
+
"ran_but_not_on_path": [{"id": i, "name": names.get(i, i)} for i in extra[:60]],
|
|
376
|
+
"ran_but_not_on_path_total": len(extra)}
|