Merge pull request #8377 from Gabrieljr42/feat/progress-library-report

feat(tools): report per-library progress in PR comparisons
This commit is contained in:
Adrià authored and GitHub committed 2026-10-24 14:01:40 +02:00
commit b930da950d
3 files changed
+141

No files matched your search

+4
View File
@@ -22,6 +22,10 @@ Comparisons identify functions by library and name. When a head group becomes a
Real removals and transitions back to stubs remain visible. Percentages retain the original snapshot totals, so eliminating duplicate accounting can change a percentage without changing available functionality. Older JSON snapshots without `shared_sources` are accepted.
PR reports include a table of changed libraries with implemented export counts, declared totals, percentages before and after, and changes in percentage points (`pp`). The table uses the same two JSON snapshots as the global report. A library appears when its counts, export names or shared-source owner change, including when its counts stay equal. Unchanged libraries are omitted, and the table lists at most 100 libraries in name order with a count of omitted rows.
New or removed libraries show `Not present` on the missing side. Libraries whose snapshots contain both counts and report zero exports show `No exports`; if either count is absent, the table shows `Counts unavailable`. Wrappers show `Shared sources` with the source library's name. These rows show `n/a` for the percentage change. The table retains the scanner's counting rules; it does not measure compatibility.
Implemented definitions are also classified as behaviourally empty when their body holds only `(void)name;` discards and at most one constant return, bare `return;` or `throw`, including a return of a local `static` wrapper that is itself behaviourally empty; wrapper calls whose arguments contain calls do not qualify. Comparisons list newly implemented behaviourally empty functions without changing any count. Older JSON snapshots without `inert_names` are accepted.
## One library
+38
View File
@@ -441,8 +441,46 @@ def compare(title, column, unit, base, head):
return lines + [""]
def library_progress(group):
if group is None:
return "Not present", None
if "shared_sources" in group:
return f'Shared sources: `{group["shared_sources"]}`', None
if "done" not in group or "todo" not in group:
return "Counts unavailable", None
total = group["done"] + group["todo"]
if not total:
return "No exports", None
percent = 100 * group["done"] / total
return f'{group["done"]}/{total} ({percent:.2f}%)', percent
def library_report(base, head):
before, after = ({group["name"]: group for group in data["groups"]} for data in (base, head))
fields = ("done", "todo", "done_names", "todo_names", "shared_sources")
rows = []
for name in sorted(before.keys() | after.keys(), key=str.casefold):
old, new = before.get(name), after.get(name)
if old is not None and new is not None and all(old.get(field) == new.get(field) for field in fields):
continue
old_text, old_percent = library_progress(old)
new_text, new_percent = library_progress(new)
delta = "n/a"
if old_percent is not None and new_percent is not None:
delta = f'{round(new_percent - old_percent, 2) or 0:+.2f} pp'
rows.append(f"| {name} | {old_text} | {new_text} | {delta} |")
if not rows:
return []
lines = ["#### Changed libraries (implemented exports)", "",
"| Library | Before | After | Change |", "| - | - | - | - |"] + rows[:REPORT_ROWS]
if len(rows) > REPORT_ROWS:
lines.append(f"| ... | {len(rows) - REPORT_ROWS} more libraries | | |")
return lines + [""]
def report(base, head):
lines = compare("System libraries", "Library", "functions", base["libraries"], head["libraries"])
lines += library_report(base["libraries"], head["libraries"])
lines += compare("GPU shader instructions", "Encoding", "instructions", base["shaders"], head["shaders"])
return "\n".join(lines)
+99
View File
@@ -107,6 +107,105 @@ class ProgressSnapshotTests(unittest.TestCase):
class ProgressTests(unittest.TestCase):
def report_library(self, name, done=(), todo=(), **extra):
return {"name": name, "label": name, "done": len(done), "todo": len(todo),
"done_names": list(done), "todo_names": list(todo), **extra}
def report_snapshot(self, *groups):
return {"libraries": progress.summarize(list(groups)), "shaders": progress.summarize([])}
def test_report_shows_each_changed_library_and_keeps_global_details(self):
pad = self.report_library("libScePad", ["Ready"], ["Pending"])
video = self.report_library("libSceVideoOut", ["A", "B", "C"], ["D"])
unchanged = self.report_library("libSceUnused", ["Keep"])
base = self.report_snapshot(video, unchanged, pad)
head = self.report_snapshot(self.report_library("libScePad", ["Ready", "Pending"]), unchanged,
self.report_library("libSceVideoOut", ["A", "B"], ["C", "D"]))
report = progress.report(base, head)
self.assertIn("**System libraries**", report)
self.assertIn("| Library | Name |", report)
self.assertIn("| libScePad | 1/2 (50.00%) | 2/2 (100.00%) | +50.00 pp |", report)
self.assertIn("| libSceVideoOut | 3/4 (75.00%) | 2/4 (50.00%) | -25.00 pp |", report)
table = report.split("#### Changed libraries (implemented exports)")[1]
self.assertNotIn("libSceUnused", table)
self.assertLess(table.index("libScePad"), table.index("libSceVideoOut"))
def test_report_includes_changed_function_names_when_counts_stay_equal(self):
base = self.report_snapshot(self.report_library("libScePad", ["Old"], ["Pending"]))
head = self.report_snapshot(self.report_library("libScePad", ["New"], ["Pending"]))
self.assertIn("| libScePad | 1/2 (50.00%) | 1/2 (50.00%) | +0.00 pp |", progress.report(base, head))
def test_report_marks_new_removed_and_empty_libraries_without_percentages(self):
base = self.report_snapshot(self.report_library("libSceRemoved", ["Ready"]),
self.report_library("libSceEmptied", ["Old"]))
head = self.report_snapshot(self.report_library("libSceAdded", ["New"], ["Pending"]),
self.report_library("libSceEmpty"), self.report_library("libSceEmptied"))
report = progress.report(base, head)
self.assertIn("| libSceAdded | Not present | 1/2 (50.00%) | n/a |", report)
self.assertIn("| libSceRemoved | 1/1 (100.00%) | Not present | n/a |", report)
self.assertIn("| libSceEmpty | Not present | No exports | n/a |", report)
self.assertIn("| libSceEmptied | 1/1 (100.00%) | No exports | n/a |", report)
def test_report_marks_missing_counts_unavailable_with_no_delta(self):
complete = {"name": "libSceLegacy", "done": 1, "todo": 0, "done_names": ["Current"]}
for missing in ({"done_names": ["Legacy"]}, {"done": 0}, {"todo": 0}, {}):
minimal = {"name": "libSceLegacy", **missing}
for before, after in ((minimal, complete), (complete, minimal)):
with self.subTest(before=before, after=after):
base = {"libraries": {"groups": [before], "percent": 0},
"shaders": {"groups": [], "percent": 0}}
head = {"libraries": {"groups": [after], "percent": 0},
"shaders": {"groups": [], "percent": 0}}
report = progress.report(base, head)
self.assertIn("Counts unavailable", report)
self.assertIn("| n/a |", report)
self.assertNotIn("No exports", report)
def test_report_omits_unchanged_minimal_snapshots(self):
minimal = {"name": "libSceLegacy"}
snapshot = {"libraries": {"groups": [minimal], "percent": 0},
"shaders": {"groups": [], "percent": 0}}
self.assertEqual(progress.report(snapshot, snapshot), "")
def test_report_labels_shared_sources_without_duplicate_counts(self):
owner = self.report_library("libScePad", ["Ready"], ["Pending"])
alias = self.report_library("libScePad.native", ["Ready"], ["Pending"])
shared = self.report_library("libScePad.native", shared_sources="libScePad")
base, head = self.report_snapshot(owner, alias), self.report_snapshot(owner, shared)
report = progress.report(base, head)
self.assertIn("| libScePad.native | 1/2 (50.00%) | Shared sources: `libScePad` | n/a |", report)
self.assertNotIn("removed", report)
reverse = progress.report(head, base)
self.assertIn("| libScePad.native | Shared sources: `libScePad` | 1/2 (50.00%) | n/a |", reverse)
changed = self.report_snapshot(owner, self.report_library("libScePad.native", shared_sources="libSceOther"))
self.assertIn("| libScePad.native | Shared sources: `libScePad` | Shared sources: `libSceOther` | n/a |",
progress.report(head, changed))
def test_report_omits_unchanged_libraries_and_preserves_shader_report(self):
library = self.report_library("libScePad", ["Ready"])
base, head = self.report_snapshot(library), self.report_snapshot(dict(library, inert_names=["Ready"]))
self.assertEqual(progress.report(base, head), "")
head["shaders"] = progress.summarize([self.report_library("VOP1", ["V_MOV_B32"])])
report = progress.report(base, head)
self.assertEqual(report, "\n".join(progress.compare("GPU shader instructions", "Encoding", "instructions",
base["shaders"], head["shaders"])))
self.assertNotIn("Changed libraries", report)
def test_report_rounds_percentage_point_change_without_negative_zero(self):
base = self.report_snapshot(self.report_library("libScePad", ["Ready"], ["A", "B"]))
head = self.report_snapshot(self.report_library("libScePad", ["Ready", "A"], ["B"]))
self.assertIn("| libScePad | 1/3 (33.33%) | 2/3 (66.67%) | +33.33 pp |", progress.report(base, head))
before = dict(self.report_library("libSceLarge", ["Ready"]), todo=100000)
after = dict(before, todo=100001)
self.assertIn("| +0.00 pp |", progress.report(self.report_snapshot(before), self.report_snapshot(after)))
def test_report_limits_library_rows(self):
head = self.report_snapshot(*(self.report_library(f"libSce{i:03}", ["Ready"]) for i in range(103)))
report = progress.report(self.report_snapshot(), head)
table = report.split("#### Changed libraries (implemented exports)")[1]
self.assertEqual(sum(line.startswith("| libSce") for line in table.splitlines()), progress.REPORT_ROWS)
self.assertIn("3 more libraries", table)
def test_raw_string_example_does_not_declare_an_export(self):
source = 'const char* example = R"help(\nint APS5_VABI Fake() { return 0; }\n)help";\n'
source += 'int APS5_VABI Real() { return 7; }\n'