Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions tools/import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,16 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu
return ordered, dict(sorted(missing.items()))


def current_stop(imports, records):
"""The first import the loader cannot resolve, in registry order."""
classes = {(record["nid"], record["library"]): record for record in records}
for nid, library in imports:
record = classes.get((nid, library))
if record is not None and record["class"] == "absent":
return record
return None


def load_names(path):
if not Path(path).is_file():
raise AuditError(f"{path}: names file not found; the audit never downloads one (see tools/nid_names.py for the source)")
Expand Down Expand Up @@ -469,7 +479,7 @@ def label(record):
return f"{text} (imported by {record['module']})" if record.get("module") else text


def render(summary, records, missing):
def render(summary, records, missing, stops):
lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"]
for kind in CLASSES:
lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references")
Expand All @@ -484,6 +494,12 @@ def render(summary, records, missing):
current = record["library"]
lines.append(f" [{current or 'library not named'}]")
lines.append(f" {label(record)}")
if stops:
lines.append("")
lines.append("Current stop (first unresolved import, in registry order)")
for entry in stops:
stop = entry["stop"]
lines.append(f" {entry['registry']}: none" if stop is None else f" {entry['registry']}: {label(stop)} [{stop['library']}]")
if missing:
lines.append("")
lines.append(f"Needed libraries with no file in --libs or --modules ({len(missing)})")
Expand Down Expand Up @@ -512,24 +528,30 @@ def main(argv=None):
output = args.json.resolve()
if any(output == path.resolve() or (args.json.exists() and args.json.samefile(path)) for path in inputs):
raise AuditError(f"{args.json}: JSON output would overwrite an input file")
imports = [entry for registry in args.registries for entry in read_registry(registry)]
registries = [(registry, read_registry(registry)) for registry in args.registries]
imports = [entry for _, entries in registries for entry in entries]
exports, libraries = built_libraries(args.libs)
module_imports, module_exports = read_modules(args.modules)
records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports)
if args.names:
attach_names(records, load_names(args.names))
summary = summarize(records, sum(record["references"] for record in records), missing)
stops = [{"registry": str(path), "stop": current_stop(entries, records)} for path, entries in registries]
if args.json:
commit, dirty = git_stamp(Path(args.source))
result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "imports": records}
json_stops = [{"registry": entry["registry"],
"nid": entry["stop"]["nid"] if entry["stop"] else None,
"library": entry["stop"]["library"] if entry["stop"] else None,
"name": entry["stop"]["name"] if entry["stop"] else None} for entry in stops]
result = {"source_commit": commit, "source_dirty": dirty, "registries": [str(path) for path in args.registries], **summary, "stops": json_stops, "imports": records}
args.json.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
except AuditError as error:
print(f"FAIL: {error}", file=sys.stderr)
return 2
except OSError as error:
print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr)
return 2
print(render(summary, records, missing))
print(render(summary, records, missing, stops))
return 1 if summary["unique_by_class"]["absent"] or missing else 0


Expand Down
25 changes: 25 additions & 0 deletions tools/tests/test_import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,31 @@ def test_clean_audit_exits_zero_and_empty_registry_is_empty(self):
self.assertEqual((code, data["references"], data["unique_imports"], data["imports"]), (0, 0, 0, []))
self.assertIn("0 references, 0 unique imports", out)

def test_current_stop_is_the_first_unresolved_import(self):
work = self.work
(work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA"))
registry = work.root / "game.json"
write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx"), ("BBBBBBBBBBB", "libSceA.prx"), ("CCCCCCCCCCC", "libSceA.prx")])
result = work.root / "audit.json"
code, out, err = work.run(registry, "--json", str(result))
self.assertEqual((code, err), (1, ""))
data = json.loads(result.read_text())
self.assertEqual(data["stops"], [{"registry": str(registry), "nid": "BBBBBBBBBBB", "library": "libSceA.prx", "name": None}])
self.assertIn("Current stop (first unresolved import, in registry order)", out)
self.assertIn("BBBBBBBBBBB", out)

def test_current_stop_is_none_when_every_import_resolves(self):
work = self.work
(work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA"))
registry = work.root / "clean.json"
write_registry(registry, [("AAAAAAAAAAA", "libSceA.prx")])
result = work.root / "audit.json"
code, out, _ = work.run(registry, "--json", str(result))
self.assertEqual(code, 0)
data = json.loads(result.read_text())
self.assertEqual(data["stops"], [{"registry": str(registry), "nid": None, "library": None, "name": None}])
self.assertIn(f" {registry}: none", out)

def test_removing_an_export_flips_the_class(self):
work = self.work
registry = work.root / "game.json"
Expand Down
Loading