chore: merge audit fixes (profsync)

This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:44:32 +02:00
commit 73d73e0f2a
6 files changed
+477 -47

No files matched your search

+212 -43
View File
@@ -945,6 +945,11 @@ class ProfileReport:
page_version: str | None = None
# An announced version searched for and found only rebuilt or re-signed.
no_genuine_copy: str | None = None
# What realign_prose has to settle before a recale or a bump can run:
# prose written under an older pin. None when not looked at. Counted to
# review, or the triage read "0 to review" on a profile whose pin could
# not move (blastem, xenia-edge, lindbergh-loader).
stale_prose: list[str] | None = None
# Skips that say the profile cannot be checked as written: a repository
# that no longer answers, no pin to read, a death the forge contradicts.
@@ -963,8 +968,10 @@ class ProfileReport:
def needs_review(self) -> int:
counts = self.counts or {}
return sum(counts.get(status, 0) for status in REVIEW_STATUSES) + len(
self.orphaned
return (
sum(counts.get(status, 0) for status in REVIEW_STATUSES)
+ len(self.orphaned)
+ len(self.stale_prose or ())
)
@@ -1305,12 +1312,22 @@ def _resolve_project_words(
if part.path not in resolved:
resolved[part.path] = resolve_project_word(part.path, tree_order())
if resolved[part.path]:
part = RefPart(resolved[part.path], part.start, part.end, part.raw)
part = replace(part, path=resolved[part.path])
rewritten.append(part)
out.append((display, ref, tokens, hashes, rewritten, citation))
return out
def _path_roles(refs: list) -> dict[str, set[str]]:
"""The roles each cited path is written under across the profile."""
roles: dict[str, set[str]] = {}
for *_, parts, _citation in refs:
for part in parts:
if part.role:
roles.setdefault(part.path, set()).add(part.role)
return roles
def build_report(
name: str, profile: dict, cache_dir: str, offline: bool = False,
text: str | None = None,
@@ -1328,20 +1345,20 @@ def build_report(
if (repo := upstream.parse_repo(url)) is not None
)
refs = []
# The repository a part says its lines are in (`upstream src/a.c:5`).
cited_roles: dict[str, str] = {}
for citation in collect_citations(profile, text):
if citation.kind == "ref":
for p in split_source_ref(citation.ref):
role, _bare = ref_role(p.path)
if role:
cited_roles.setdefault(strip_repo_word(p.path, known), role)
entry = citation.entry
display = str(entry.get("name") or "") or citation.field
if citation.label:
display += f" [{citation.label}]"
# The repository a part says its lines are in (`upstream
# src/a.c:5`) stays on that part: the same path can be cited
# under both roles in one profile.
parts = [
RefPart(strip_repo_word(p.path, known), p.start, p.end, p.raw)
RefPart(
strip_repo_word(p.path, known), p.start, p.end, p.raw,
ref_role(p.path)[0],
)
for p in split_source_ref(citation.ref)
]
refs.append((
@@ -1353,6 +1370,19 @@ def build_report(
citation.field, citation.ref, [], [],
list(citation.parts), citation,
))
# A part that names no repository is looked up first where its path is
# cited elsewhere: a note citing `src/a.c:12` beside source_refs that
# all read `upstream src/a.c` means that tree.
# A path cited under both roles lends neither, and such a part is
# compared in every repository rather than in whichever comes first.
path_roles = _path_roles(refs)
def role_of(part: RefPart) -> str:
if part.role:
return part.role
lent = path_roles.get(part.path, ())
return next(iter(lent)) if len(lent) == 1 else ""
# Directories the profile already cites, deepest first, to settle a bare
# filename that several trees of one repository carry.
cited_dirs: set[str] = set()
@@ -1448,7 +1478,7 @@ def build_report(
pin_trees: dict[str, list[str] | None] = {}
def _locate(
path: str, start: int | None = None, tokens: tuple = ()
path: str, start: int | None = None, tokens: tuple = (), wanted: str = ""
) -> tuple[RepoView, str]:
"""Repository and real path carrying a cited path, HEAD before pin.
@@ -1539,7 +1569,6 @@ def build_report(
return 1 if line.strip() else 0
# A part that names its repository is looked up there first.
wanted = cited_roles.get(path, "")
ordered = sorted(views, key=lambda v: _view_role(v) != wanted) if wanted else views
best = None
for candidate in candidates:
@@ -1572,11 +1601,11 @@ def build_report(
return (best[1], best[2]) if best else (primary, path)
def resolve_path(
path: str, start: int | None = None, tokens: tuple = ()
path: str, start: int | None = None, tokens: tuple = (), role: str = ""
) -> tuple[RepoView, str]:
key = (path, start, tokens)
key = (path, start, tokens, role)
if key not in owners:
owners[key] = _locate(path, start, tokens)
owners[key] = _locate(path, start, tokens, role)
return owners[key]
def _pin_tree_for(view: RepoView) -> list[str] | None:
@@ -1634,14 +1663,14 @@ def build_report(
)
return context[key]
def rename_getter(path: str) -> tuple[str | None, list[str]]:
def rename_getter(path: str, role: str = "") -> tuple[str | None, list[str]]:
"""Resolved only when a cited file has vanished, then memoised.
Every declared repository is searched, not just the one that owns the
path: a profile citing both a port and its upstream may point at a file
that only the other one carries.
"""
owner, actual = resolve_path(path)
owner, actual = resolve_path(path, role=role)
ordered = [owner] + [v for v in views if v is not owner]
candidates: list[str] = []
for view in ordered:
@@ -1673,6 +1702,7 @@ def build_report(
start: int | None = None,
tokens: Iterable[str] = (),
forced: RepoView | None = None,
role: str = "",
) -> list[str] | None:
"""Lines for one path at one revision, read once per run.
@@ -1680,10 +1710,11 @@ def build_report(
on identity.
"""
tokens = tuple(tokens)
key = (which, path, start, tokens, forced and forced.repo.slug)
key = (which, path, start, tokens, forced and forced.repo.slug, role)
if key not in lines_cache:
view, actual = (
(forced, path) if forced else resolve_path(path, start, tokens)
(forced, path) if forced
else resolve_path(path, start, tokens, role)
)
sha = view.pin if which == PIN else view.head
found = fetch_from(view.repo, sha, actual)
@@ -1701,9 +1732,11 @@ def build_report(
start: int | None = None,
tokens: Iterable[str] = (),
forced: RepoView | None = None,
role: str = "",
) -> tuple[str | None, str, str]:
view, actual = (
(forced, path) if forced else resolve_path(path, start, tuple(tokens))
(forced, path) if forced
else resolve_path(path, start, tuple(tokens), role)
)
slug = view.repo.slug if view is not primary else None
return slug, upstream.raw_url(view.repo, view.head, actual), actual
@@ -1715,12 +1748,20 @@ def build_report(
both mamedev/mame and its libretro fork hold, with the same delimited
token on the cited line, so no single attribution can be right for all
of them. Taking the best outcome across repositories settles it.
A part that names its repository is judged there alone when that
repository carries the path: hypseus-singe cites lair.cpp under
`source` for one entry and `upstream` for the next.
"""
owner, _actual = resolve_path(part.path, part.start, tuple(tokens))
owner, _actual = resolve_path(
part.path, part.start, tuple(tokens), role_of(part)
)
carrying = [
view for view in views
if fetch_from(view.repo, view.pin, part.path) is not None
]
named = [view for view in carrying if _view_role(view) == part.role]
if part.role and named:
carrying = named
live = {
id(view) for view in carrying
if fetch_from(view.repo, view.head, part.path) is not None
@@ -1732,7 +1773,7 @@ def build_report(
lambda which, path, start=None, toks=(), _v=view: fetch(
which, path, start, toks, _v
),
rename_getter,
lambda path: rename_getter(path, role_of(part)),
lambda path, start=None, toks=(), _v=view: describe(
path, start, toks, _v
),
@@ -1753,7 +1794,15 @@ def build_report(
if best is None or rank < best[0] or (rank == best[0] and view is owner):
best = (rank, result)
return best[1] if best else anchor_part(
part, fetch, rename_getter, describe, tokens
part,
lambda which, path, start=None, toks=(): fetch(
which, path, start, toks, role=role_of(part)
),
lambda path: rename_getter(path, role_of(part)),
lambda path, start=None, toks=(): describe(
path, start, toks, role=role_of(part)
),
tokens,
)
# Comparing a revision with itself anchors every ref whatever it cites, so
@@ -1763,7 +1812,9 @@ def build_report(
def self_checked(part: RefPart, tokens: Sequence[str]) -> bool:
if report.pinned_tag:
return True
view, _ = resolve_path(part.path, part.start, tuple(tokens))
if not part.role and len(path_roles.get(part.path, ())) > 1:
return all(view.pin == view.head for view in views)
view, _ = resolve_path(part.path, part.start, tuple(tokens), role_of(part))
return view.pin == view.head
def judge(
@@ -1779,11 +1830,15 @@ def build_report(
)
if flag:
result = verify_at_pin(
part, fetch(PIN, part.path, part.start, tokens), tokens, hashes
part,
fetch(PIN, part.path, part.start, tokens, role=role_of(part)),
tokens, hashes,
)
# Checked at its own revision, the part still lies in one tree:
# the report names it, as a comparison does.
owner, _actual = resolve_path(part.path, part.start, tuple(tokens))
owner, _actual = resolve_path(
part.path, part.start, tuple(tokens), role_of(part)
)
if result.repo is None and owner is not primary:
result = replace(result, repo=owner.repo.slug)
else:
@@ -1791,7 +1846,9 @@ def build_report(
# A forge that refused the request said nothing about the file:
# git.eden-emu.dev answers 403 to scripts, and nine citations of
# eden-nightly read "absent at HEAD" for a file that exists.
owner, _actual = resolve_path(part.path, part.start, tuple(tokens))
owner, _actual = resolve_path(
part.path, part.start, tuple(tokens), role_of(part)
)
if (
result.status in ("GONE", "AMBIGUOUS")
and (owner.repo.host, owner.repo.slug) in mute
@@ -1914,6 +1971,8 @@ def format_report(report: ProfileReport, changed_only: bool = False) -> str:
f"{count} {status.lower()}" for status, count in sorted(report.counts.items())
)
lines.append(f" {len(report.entries)} refs: {summary}")
for line in report.stale_prose or ():
lines.append(f" prose under an older pin: {line}")
lines.append(f" {report.needs_review()} to review")
return "\n".join(lines)
@@ -2150,6 +2209,12 @@ def drift_score(
"""Rank profiles by how far they have drifted from their pin."""
counts = report.counts or {}
score = sum(DRIFT_WEIGHTS.get(k, 0) * v for k, v in counts.items())
# Prose a reader has to settle weighs as an ambiguous ref, prose
# --realign-prose moves by itself as a shifted one.
score += sum(
DRIFT_WEIGHTS["SHIFTED" if line.startswith("would recale") else "AMBIGUOUS"]
for line in report.stale_prose or ()
)
if version is not None and not version.tag_matches_declared:
if version.latest_release and version.latest_release != version.declared:
score += 10
@@ -2827,12 +2892,41 @@ def _scalar_values(document: dict) -> dict[str, str]:
}
def _pin_of(document: dict, field: str, mode: str = "") -> str | None:
"""A pin as the profile records it, read for one build mode when keyed."""
def _pin_of(
document: dict, field: str, mode: str = "",
repo: upstream.Repo | None = None,
) -> str | None:
"""A pin as the profile records it, read for one build mode when keyed.
With `repo`, a pin the document declares for another repository under
the field and mode does not count. A past revision can pin another
repository under the same field: xenia-edge's source_commit named the
has207/xenia-edge sha until the field became a dict keyed by mode, and
read for the libretro mode that old string passed for a pin of the
GitLab port that moved under unchanged prose.
"""
value = document.get(field)
if isinstance(value, dict):
value = value.get(mode) if mode else None
return value if isinstance(value, str) and value else None
if not (isinstance(value, str) and value):
return None
if repo is not None:
url_field = field.removesuffix("_commit")
declared = {
(mode_key, url)
for name, mode_key, url in declared_repositories(document)
if name == url_field
}
url = next(
(u for m, u in declared if m == mode),
next((u for m, u in declared if not m), None),
)
owner = upstream.parse_repo(url) if url else None
if owner is not None and (owner.host, owner.slug.lower()) != (
repo.host, repo.slug.lower()
):
return None
return value
_CITATION_RUN_RE = re.compile(
@@ -2856,7 +2950,8 @@ def _scalar_citations(text: str | None) -> tuple[str, ...] | None:
def _writing_pins(
revisions: list[tuple[str, dict]], intro_sha: str, field: str, mode: str = ""
revisions: list[tuple[str, dict]], intro_sha: str, field: str, mode: str = "",
repo: upstream.Repo | None = None,
) -> list[str]:
"""Distinct pins committed while the scalar held its current text.
@@ -2875,7 +2970,7 @@ def _writing_pins(
return []
pins: list[str] = []
for position in range(index, -1, -1):
value = _pin_of(revisions[position][1], field, mode)
value = _pin_of(revisions[position][1], field, mode, repo)
if value and value not in pins:
pins.append(value)
return pins
@@ -2988,7 +3083,9 @@ def _writing_pairs(
current = _pin_of(document, f"{pin_field}_commit", mode)
if not current:
continue
written = _writing_pins(revisions, intro_sha, f"{pin_field}_commit", mode)
written = _writing_pins(
revisions, intro_sha, f"{pin_field}_commit", mode, repo
)
if attested in written:
written = [attested]
if len(written) > 1:
@@ -3096,13 +3193,34 @@ def realign_prose(
# Fields whose writing pin lies past a revision that cannot be read:
# where their text came in is unknown, so they are not moved.
unknown: dict[str, str] = {}
# A revision that does not parse is passed over unless the pins differ
# on its two sides while the field's citations changed across it: then
# the text may have come in there under either pin. Otherwise nothing
# it could hold changes the reading. lindbergh-loader
# committed invalid YAML once (df7346b3), fixed the same day, and every
# note older than that commit stayed blocked for good.
gap: str | None = None
for sha in history:
if not any(alive.values()):
break
past = _git_file_at(path, sha)
if past is None:
unknown = {field: sha for field, live in alive.items() if live}
break
gap = gap or sha
continue
if gap:
newer = revisions[-1][1] if revisions else document
moved = any(
newer.get(pin) != past.get(pin)
for pin in ("source_commit", "upstream_commit")
)
values = _scalar_values(past)
for field, live in alive.items():
if live and moved and _scalar_citations(values.get(field)) != (
_scalar_citations(current_values.get(field))
):
unknown[field] = gap
alive[field] = False
gap = None
revisions.append((sha, past))
values = _scalar_values(past)
for field in fields:
@@ -3114,6 +3232,10 @@ def realign_prose(
intro[field] = sha
else:
alive[field] = False
if gap:
# The oldest revisions do not parse: where the text came in is past
# what can be read.
unknown.update({field: gap for field, live in alive.items() if live})
messages: list[str] = []
jobs: list[tuple[Citation, dict]] = []
@@ -3384,14 +3506,21 @@ def _check_quota(count: int, offline: bool) -> None:
raise SystemExit(1)
def _stale_prose_blocks(name: str, path: Path, args: argparse.Namespace) -> bool:
def _stale_prose_blocks(
name: str, path: Path, args: argparse.Namespace,
known: list[str] | None = None,
) -> bool:
"""Whether prose written under an older pin has to move first.
Anchored from the current pin, a recale or a bump follows someone else's
code (boom3 FileSystem.cpp:2125, written at b810234e, an unrelated
OpenFileRead at 132dfddb). It moves first, from the pin it was written at.
`known` is the answer the report already holds for the same text.
"""
stale = realign_prose(path, args.cache_dir, args.offline, dry_run=True)
stale = (
known if known is not None
else realign_prose(path, args.cache_dir, args.offline, dry_run=True)
)
if stale:
print(
f"{name}: {len(stale)} prose citation(s) written under an older pin; "
@@ -3401,6 +3530,28 @@ def _stale_prose_blocks(name: str, path: Path, args: argparse.Namespace) -> bool
return bool(stale)
def _read_stale_prose(
name: str, path: Path, args: argparse.Namespace, failed: set[str]
) -> list[str]:
"""What --realign-prose would do or refuse on this profile, as a report.
Read on every pass, not only before a write: prose that blocks the pin
is review work whether or not this run tries to move it.
"""
try:
return realign_prose(path, args.cache_dir, args.offline, dry_run=True)
except upstream.RateLimitError:
raise
except upstream.NotCachedError as exc:
# Unread offline, as a report is: not clean, and not a failure.
return [f"not read offline: {exc}"]
except upstream.UpstreamError as exc:
# Not read is not clean: the bump would refuse on the same error.
print(f"{name}: prose not read, {exc}", file=sys.stderr)
failed.add(name)
return [f"not read: {exc}"]
def _backfill(
args: argparse.Namespace, name: str, profile: dict, report: ProfileReport,
path: Path,
@@ -3434,7 +3585,11 @@ def _apply_writes(
if not args.dry_run:
path.write_bytes(target.read_bytes())
return
if not (args.rebase_refs or args.bump_commit) or _stale_prose_blocks(name, path, args):
# A backfill has just written a pin: the report's reading predates it.
known = None if args.backfill_commits else report.stale_prose
if not (args.rebase_refs or args.bump_commit) or _stale_prose_blocks(
name, path, args, known
):
return
with tempfile.TemporaryDirectory() as scratch:
# Always work on a copy. A dry run reports what it left there, a real
@@ -3600,6 +3755,7 @@ def _print_triage(
) -> None:
"""Profiles ranked by drift, worst first."""
ranked = []
sampled = 0
for report in reports:
profile = selected[report.name]
version = None
@@ -3621,20 +3777,31 @@ def _print_triage(
refused = "commit history unreachable"
print(f"{report.name}: {exc}", file=sys.stderr)
if len(paths) > TRIAGE_PATH_SAMPLE and not refused:
print(
f"{report.name}: commit count sampled on "
f"{TRIAGE_PATH_SAMPLE} of {len(paths)} paths",
file=sys.stderr,
)
sampled += 1
ranked.append((drift_score(report, version, commits), report, refused))
if sampled:
# One line for the run: a notice per profile buried the ranking
# under 254 lines of stderr.
print(
f"commit count sampled on {TRIAGE_PATH_SAMPLE} cited paths for "
f"{sampled} profiles citing more",
file=sys.stderr,
)
print(f"{'score':>5s} {'profile':30s} state (worst first)")
pending = 0
for score, report, refused in sorted(ranked, key=lambda item: -item[0]):
state = report.skipped or f"{report.needs_review()} to review"
stale = len(report.stale_prose or ())
if stale and not report.skipped:
state += f", {stale} prose under an older pin"
unchecked = (report.counts or {}).get("UNCHECKED", 0)
if unchecked and not report.skipped:
state += f", {unchecked} unchecked"
if refused:
state += f", {refused}"
pending += bool(report.needs_review())
print(f"{score:5d} {report.name:30s} {state}")
print(f"{len(ranked)} profiles, {pending} with something to review")
def _refuse_flag_combinations(args: argparse.Namespace) -> None:
@@ -3789,6 +3956,8 @@ def main() -> None:
name=name, entries=[], counts={}, skipped=f"upstream error: {exc}"
)
failed.add(name)
if not report.skipped and profile_path.is_file():
report.stale_prose = _read_stale_prose(name, profile_path, args, failed)
if args.check_version and profile.get("version_source"):
report.declared_version = str(profile.get("core_version") or "") or None
report.page_version = page_version(profile, args.cache_dir, args.offline)
+4
View File
@@ -261,6 +261,10 @@ def link_prose_citations(profile: dict, text: str) -> str:
continue
if "/" not in path and path not in structured:
continue
role = parts[0].role
if role and (role_sources(profile).get(role) or (None,))[0] != repo:
# `upstream a.c:5` names a repository this link would not reach.
continue
if any(lo <= path_span[0] < hi for lo, hi in guarded):
continue
for index, ((lo, hi), part) in enumerate(zip(spans, parts)):
+20 -2
View File
@@ -39,6 +39,10 @@ class RefPart:
start: int | None
end: int | None
raw: str = ""
# The repository the part names (`upstream src/a.c:5`), empty when it
# names none. Carried per part: one profile cites the same path in both
# repositories, hypseus-singe's lair.cpp under `source` and `upstream`.
role: str = ""
@property
def last(self) -> int | None:
@@ -247,6 +251,19 @@ def _prose_external(
return not lead or lead[-1] in _PROSE_LIST_DELIMITERS
def _prose_role(line: str, path_start: int) -> str:
"""The role word written right before a prose path, empty when none.
Prose follows the source_ref convention: `(upstream retro_host.c:196)`
names the original emulator, `ref: source src/devices/floppy.cpp:1656`
the repository declared as source. Read as a linking word or a project
name, the word was dropped or made the citation external.
"""
before = line[:path_start].rstrip()
word = before[max(before.rfind(" "), before.rfind("\t"), before.rfind("(")) + 1:]
return word if word in REPO_ROLES else ""
def resolve_project_word(path: str, trees: Iterable[list[str] | None]) -> str | None:
"""Path of `project file` inside a declared tree, None when out of reach.
@@ -333,7 +350,8 @@ def _prose_runs(
):
path_start -= 1
path = "." + path
if _prose_external(line, path_start, known):
role = _prose_role(line, path_start)
if not role and _prose_external(line, path_start, known):
# Kept with its project word, the source_ref convention: the
# report looks for the project inside the declared trees
# (mupen64plus-ae vendors `rice Config.cpp`) and calls the
@@ -358,7 +376,7 @@ def _prose_runs(
for lo, hi in spans:
rng = line[lo:hi]
a, _, b = rng.partition("-")
parts.append(RefPart(path, int(a), int(b or a), rng))
parts.append(RefPart(path, int(a), int(b or a), rng, role))
yield (
number, path_span, tuple(spans), tuple(parts),
line[path_start:end], end > boundary,
+2 -1
View File
@@ -3560,7 +3560,8 @@ class TriageSurvivesAForgeRefusal(unittest.TestCase):
contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(io.StringIO()):
profile_sync._print_triage(args, selected, reports)
lines = out.getvalue().splitlines()
# A header row and a closing count frame the ranking.
lines = out.getvalue().splitlines()[1:-1]
self.assertEqual(len(lines), 2)
self.assertTrue(any("eden" in l and "unreachable" in l for l in lines))
+230
View File
@@ -2473,6 +2473,28 @@ class TestWriteDryRun(unittest.TestCase):
)
self.assertIn([(repo, "old", "new")], readings)
def test_a_pin_of_another_repository_is_no_writing_pin(self):
"""xenia-edge: source_commit named the has207/xenia-edge sha until it
became a dict whose libretro key pins a GitLab port. Read for that
mode, the old string passed for a pin of the port that moved under
unchanged prose, and the bump stayed refused for good."""
edge = "https://github.com/has207/xenia-edge"
port = "https://gitlab.com/x/xenia-libretro"
modes = {"standalone": edge, "libretro": port}
revisions = [
("c2", {"source": modes, "notes": "x",
"source_commit": {"standalone": "e1", "libretro": "p1"}}),
("c1", {"source": edge, "notes": "x", "source_commit": "e1"}),
]
document = dict(revisions[0][1])
repos = [("source", mode, profile_sync.upstream.parse_repo(url))
for mode, url in modes.items()]
readings, ambiguous = profile_sync._writing_pairs(
document, repos, revisions, "c1"
)
self.assertEqual(readings, [[]])
self.assertEqual(ambiguous, [])
def test_stale_prose_holds_the_pin(self):
"""boom3: a note written at an older pin is not anchored from this one."""
before = self.path.read_text()
@@ -3823,6 +3845,46 @@ class TestRealignProse(unittest.TestCase):
self.assertTrue(all(m.startswith("read again: notes:") for m in messages))
self.assertIn("(a.c:10)", self.path.read_text())
def _repo_with_a_broken_revision(self, broken_pin: str):
self._git("init", "-q")
self._git("config", "user.email", "t@t")
self._git("config", "user.name", "t")
written = PROSE_SAMPLE.replace('"pin"', '"oldpin"')
self._commit(written, "profile")
broken = PROSE_SAMPLE.replace('"pin"', f'"{broken_pin}"')
self._commit(broken + "files: [\n", "invalid yaml")
self._commit(written, "repair")
self.path.write_text(PROSE_SAMPLE, encoding="utf-8")
self.files[("oldpin", "a.c")] = ["x"] * 9 + ["subject"]
self.files[("pin", "a.c")] = ["x"] * 13 + ["subject"]
self.files[("oldpin", "b.c")] = list("pqrstuvwx")
self.files[("pin", "b.c")] = list("pqrstuvwx")
def test_a_revision_that_does_not_parse_is_passed_over(self):
"""lindbergh-loader committed invalid YAML once, repaired the same
day under the same pin. Every older note was reported unreadable
and the recale stayed refused for good."""
self._repo_with_a_broken_revision("oldpin")
messages = profile_sync.realign_prose(self.path, self.tmp.name, dry_run=True)
self.assertFalse(any("unreadable" in m for m in messages), messages)
self.assertTrue(any("would recale" in m for m in messages), messages)
def test_a_revision_that_does_not_parse_still_blocks_when_it_can_matter(self):
"""The pins differ across the gap and the note's citations changed:
the text may have come in there under either pin."""
self._git("init", "-q")
self._git("config", "user.email", "t@t")
self._git("config", "user.name", "t")
self._commit(
PROSE_SAMPLE.replace('"pin"', '"older"').replace("a.c:10", "a.c:3"),
"profile",
)
self._commit(PROSE_SAMPLE + "files: [\n", "invalid yaml")
self._commit(PROSE_SAMPLE.replace('"pin"', '"oldpin"'), "repair")
self.path.write_text(PROSE_SAMPLE, encoding="utf-8")
messages = profile_sync.realign_prose(self.path, self.tmp.name, dry_run=True)
self.assertTrue(any("unreadable" in m for m in messages), messages)
def test_scalar_written_at_the_current_pin_is_left_alone(self):
self._git("init", "-q")
self._git("config", "user.email", "t@t")
@@ -3940,6 +4002,25 @@ class TestOutputModesApplyTheirFlags(unittest.TestCase):
code, _, _ = self._main("--strict")
self.assertEqual(code, 0)
def test_strict_fails_on_prose_that_blocks_the_pin(self):
"""blastem read "0 to review" while its prose refused every bump."""
from unittest import mock
with tempfile.TemporaryDirectory() as tmp:
for name in ("clean", "drift"):
(Path(tmp) / f"{name}.yml").write_text("emulator: x\n")
stale = {"clean": ["read again: notes: a.c:5 (pin moved)"], "drift": []}
with mock.patch.object(
profile_sync, "realign_prose",
side_effect=lambda path, *a, **k: stale[path.stem],
):
code, out, _ = self._main(
"--changed-only", "--strict", "--emulators-dir", tmp,
)
self.assertEqual(code, 1)
self.assertIn("clean", out)
self.assertIn("prose under an older pin", out)
def test_extras_are_refused_with_json(self):
code, _, err = self._main("--json", "--check-version")
self.assertEqual(code, 1)
@@ -4340,5 +4421,154 @@ class EachCitationNamesTheCodeThatAsks(unittest.TestCase):
self.assertEqual(self._attribute("source emu/memory.c:2"), "source emu/memory.c:2")
class ARoleIsReadPerPart(unittest.TestCase):
"""The role a part writes (`source a.c:3`) was stored per path, first met
first kept: hypseus-singe's `upstream lair.cpp` decided where every later
`source lair.cpp` was read, and correct refs were reported MOVED."""
setUp = TestBuildReport.setUp
tearDown = TestBuildReport.tearDown
MD5A = "0123456789abcdef0123456789abcdef"
MD5B = "fedcba9876543210fedcba9876543210"
def _report(self, files: str, notes: str = "") -> ProfileReport:
# The port inserted two lines above its copy of the table since its
# pin, while the original sits on HEAD with the same table.
trees = {
("libretro/x", "pins"): {"a.c": ["pad", "pad", f"rom {self.MD5B}"]},
("libretro/x", "heads"): {"a.c": ["pad", "pad", "new", "new",
f"rom {self.MD5B}"]},
("alek/x", "pinu"): {"a.c": [f"rom {self.MD5A}", "x",
f"rom {self.MD5B}"]},
}
profile_sync.upstream.fetch_file = (
lambda repo, sha, path, cache_dir, offline=False:
trees.get((repo.slug, sha), {}).get(path)
)
profile_sync.upstream.list_tree = (
lambda repo, sha, cache_dir, offline=False:
(sorted(trees.get((repo.slug, sha), {})), False)
)
profile_sync.upstream.resolve_head = (
lambda repo, cache_dir, offline=False, branch=None:
"heads" if repo.owner == "libretro" else "pinu"
)
profile_sync.upstream.compare = (
lambda repo, base, head, cache_dir, offline=False:
profile_sync.CompareResult([], False)
)
text = (
'emulator: T\n'
'source: "https://github.com/libretro/x"\n'
'upstream: "https://github.com/alek/x"\n'
'source_commit: "pins"\n'
'upstream_commit: "pinu"\n'
+ (f"notes: |\n {notes}\n" if notes else "")
+ "files:\n" + files
)
profile = yaml.safe_load(text)
with contextlib.redirect_stderr(io.StringIO()):
return build_report("t", profile, self.dir, text=text)
def test_a_part_naming_its_repository_is_judged_there(self):
report = self._report(
" - name: a.rom\n"
f" md5: {self.MD5A}\n"
' source_ref: "upstream a.c:1"\n'
" - name: b.rom\n"
f" md5: {self.MD5B}\n"
' source_ref: "source a.c:3"\n'
)
parts = {e.name: e.parts[0] for e in report.entries}
self.assertEqual(parts["a.rom"].status, "ANCHORED")
self.assertEqual(parts["a.rom"].repo, "alek/x")
# Read in the original, where line 3 still holds the table, the
# port's own move went unseen.
self.assertEqual(parts["b.rom"].status, "SHIFTED")
self.assertEqual(parts["b.rom"].start, 5)
self.assertIsNone(parts["b.rom"].repo)
def test_a_prose_source_word_is_a_role_not_a_project(self):
report = self._report(
" - name: b.rom\n"
f" md5: {self.MD5B}\n"
' source_ref: "source a.c:3"\n',
notes="ref: source a.c:3, more",
)
prose = [e for e in report.entries if e.kind == "prose"]
self.assertEqual(len(prose), 1)
self.assertNotEqual(prose[0].parts[0].status, "EXTERNAL")
self.assertEqual(prose[0].parts[0].part.role, "source")
def test_a_prose_upstream_word_names_the_repository(self):
report = self._report(
" - name: b.rom\n"
f" md5: {self.MD5B}\n"
' source_ref: "source a.c:3"\n',
notes="the original reads it (upstream a.c:1).",
)
prose = [e for e in report.entries if e.kind == "prose"]
self.assertEqual(prose[0].parts[0].part.role, "upstream")
self.assertEqual(prose[0].parts[0].repo, "alek/x")
def test_the_prose_grammar_keeps_the_role(self):
from sourceref import _prose_runs
runs = list(_prose_runs(
"# ref: source src/a.cpp:1656-1685, 1709-1738, upstream b.c:5"
))
roles = [(p.path, p.role) for run in runs for p in run[3]]
self.assertEqual(roles, [
("src/a.cpp", "source"), ("src/a.cpp", "source"), ("b.c", "upstream"),
])
class TheTriageCountsWhatBlocksThePin(unittest.TestCase):
"""Prose written under an older pin refuses the recale and the bump, yet
the triage, --changed-only and --strict read "0 to review" on blastem,
xenia-edge and lindbergh-loader. And one stderr notice per profile put
254 lines above an unlabelled ranking."""
def _report(self, name: str, paths: int, stale: list[str] | None = None):
entries = [
EntryReport(f"f{i}", f"d/{i}.c:1", "ANCHORED", [
PartResult(RefPart(f"d/{i}.c", 1, 1, "1"), "ANCHORED",
None, None, None, [])
])
for i in range(paths)
]
return ProfileReport(
name=name, pin="p", head="h", entries=entries,
counts={"ANCHORED": paths}, stale_prose=stale,
)
def test_stale_prose_is_review_work(self):
report = self._report("b", 1, ["read again: notes: a.c:5 (pin moved)"])
self.assertEqual(report.needs_review(), 1)
self.assertIn("prose under an older pin", format_report(report))
self.assertEqual(report_to_dict(report)["needs_review"], 1)
def test_the_ranking_is_labelled_and_the_sampling_said_once(self):
reports = [
self._report("clean", 7),
self._report("stuck", 7, ["read again: notes: a.c:5 (pin moved)"]),
]
selected = {r.name: {"source": "https://github.com/o/n"} for r in reports}
args = argparse.Namespace(cache_dir="unused", offline=True)
out, err = io.StringIO(), io.StringIO()
with mock.patch.object(profile_sync, "check_version", return_value=None), \
mock.patch.object(profile_sync.upstream, "commits_touching",
return_value=0), \
contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
profile_sync._print_triage(args, selected, reports)
lines = out.getvalue().splitlines()
self.assertIn("profile", lines[0])
self.assertIn("stuck", lines[1])
self.assertIn("1 to review, 1 prose under an older pin", lines[1])
self.assertIn("0 to review", lines[2])
self.assertEqual(lines[-1], "2 profiles, 1 with something to review")
self.assertEqual(len(err.getvalue().splitlines()), 1, err.getvalue())
if __name__ == "__main__":
unittest.main()
+9 -1
View File
@@ -415,7 +415,15 @@ not declare, so no revision can confirm it), `UNCHECKED` (a pin equal to
HEAD is judged on self-consistency, and an entry with neither a hash nor
a name gives that check nothing to look for). An entry carries the worst
status of its parts. Only `CHANGED`, `GONE` and `AMBIGUOUS` count as
needing a re-read. A zero next to a row of `unchecked` is not a proof.
needing a re-read, with each prose citation `--realign-prose` would move or
refuse (written under an older pin, it holds the pin back). A zero next to
a row of `unchecked` is not a proof.
A part opening with `source` or `upstream`, in a source_ref or in prose,
is read in that repository. The word belongs to the part, not to its path:
one profile can cite the same file under both. `--triage` prints a header,
ranks worst first and closes on the number of profiles with something to
review.
A single cited line is often not distinctive, so the anchor widens by
steps of ±3, ±6, ±12, ±25 and ±50 lines until it is unique. Three further