Files
AnyPS5/tools/tests/test_nid_names.py
T
2026-10-29 00:30:36 +13:00

652 lines
35 KiB
Python

import contextlib
import http.server
import io
import json
import os
import subprocess
import tempfile
import sys
import threading
import unittest
from pathlib import Path, PureWindowsPath
from unittest.mock import patch
from urllib.error import URLError
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import nid_names
class DatabaseCacheTests(unittest.TestCase):
@contextlib.contextmanager
def database_server(self, payload):
state = {"payload": payload, "requests": 0}
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
state["requests"] += 1
body = state["payload"]
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=lambda: server.serve_forever(poll_interval=0.01), daemon=True)
thread.start()
try:
with patch.object(nid_names, "DB_URL", f"http://127.0.0.1:{server.server_port}/aerolib.csv"):
yield state
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
self.assertFalse(thread.is_alive())
def test_unusable_successful_downloads_are_not_cached_and_retry(self):
valid = b"\xef\xbb\xbf# database\nmFq1M6vw-JM ACProcessMain\n"
for payload in (b"", b"# comment only\n", b"not-a-database\n", b"<!doctype html>\n<html>unavailable</html>\n"):
with self.subTest(payload=payload), tempfile.TemporaryDirectory() as directory, self.database_server(payload) as state:
cache = Path(directory) / "aerolib.csv"
with self.assertRaisesRegex(ValueError, "no valid NID records") as raised:
nid_names.load_db(cache)
self.assertIn(str(cache), str(raised.exception))
self.assertEqual(list(cache.parent.iterdir()), [])
state["payload"] = valid
self.assertEqual(nid_names.load_db(cache), {"mFq1M6vw-JM": "ACProcessMain"})
self.assertEqual(cache.read_bytes(), valid)
self.assertEqual(state["requests"], 2)
self.assertEqual(nid_names.load_db(cache), {"mFq1M6vw-JM": "ACProcessMain"})
self.assertEqual(state["requests"], 2)
self.assertEqual(list(cache.parent.iterdir()), [cache])
def test_download_content_errors_are_reported_by_cli(self):
with tempfile.TemporaryDirectory() as directory, self.database_server(b""):
cache = Path(directory) / "aerolib.csv"
stdout, stderr = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr), self.assertRaises(SystemExit) as raised:
nid_names.main(["--db", str(cache), "--nid", "mFq1M6vw-JM", "--json"])
self.assertEqual(raised.exception.code, 2)
self.assertIn(str(cache), stderr.getvalue())
self.assertIn("no valid NID records", stderr.getvalue())
self.assertNotIn("Traceback", stderr.getvalue())
self.assertEqual(stdout.getvalue(), "")
self.assertFalse(cache.exists())
def test_existing_database_files_keep_permissive_parsing(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "custom.csv"
for payload, expected in ((b"", {}), (b"# comment\n", {}), (b"legacy customName\n", {"legacy": "customName"})):
with self.subTest(payload=payload):
cache.write_bytes(payload)
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=AssertionError("unexpected download")):
self.assertEqual(nid_names.load_db(cache), expected)
self.assertEqual(cache.read_bytes(), payload)
def long_cache_path(self, directory):
root = Path(directory)
if os.name == "nt":
root = Path("\\\\?\\" + str(root.resolve()))
limit = os.pathconf(root, "PC_NAME_MAX") if os.name == "posix" else 255
cache = root / ("n" * (limit - len(".csv")) + ".csv")
cache.touch()
cache.unlink()
return cache
def test_valid_long_cache_filename_can_be_downloaded(self):
with tempfile.TemporaryDirectory() as directory:
cache = self.long_cache_path(directory)
def complete(url, filename):
self.assertEqual(url, nid_names.DB_URL)
self.assertEqual(Path(filename).parent, cache.parent)
self.assertFalse(cache.exists())
Path(filename).write_text("AAAAAAAAAAA sceComplete\n", encoding="utf-8")
try:
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete) as download:
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceComplete"})
download.assert_called_once()
self.assertEqual(list(cache.parent.iterdir()), [cache])
with patch.object(nid_names.urllib.request, "urlretrieve") as download:
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceComplete"})
download.assert_not_called()
finally:
cache.unlink(missing_ok=True)
def test_long_cache_filename_preserves_interruption_cleanup_and_retry(self):
with tempfile.TemporaryDirectory() as directory:
cache = self.long_cache_path(directory)
def interrupted(url, filename):
Path(filename).write_text("AAAAAAAAAAA scePartial\n", encoding="utf-8")
raise URLError("connection interrupted")
def complete(url, filename):
Path(filename).write_text("AAAAAAAAAAA sceComplete\n", encoding="utf-8")
try:
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=interrupted):
with self.assertRaises(URLError):
nid_names.load_db(cache)
self.assertFalse(cache.exists())
self.assertEqual(list(cache.parent.iterdir()), [])
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete):
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceComplete"})
self.assertEqual(list(cache.parent.iterdir()), [cache])
finally:
cache.unlink(missing_ok=True)
def test_empty_symbol_filters_do_not_open_database(self):
with tempfile.TemporaryDirectory() as directory:
for option in ("--name", "--symbol", "--na", "--sym"):
for json_output in (False, True):
with self.subTest(option=option, json_output=json_output):
result = subprocess.run(
[sys.executable, str(Path(nid_names.__file__)), option, "--db", directory,
*(["--json"] if json_output else [])], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
if json_output:
self.assertEqual(json.loads(result.stdout), {"symbols": []})
else:
self.assertEqual(result.stdout.split(), ["NAME", "NID"])
def test_empty_nid_filters_do_not_open_database(self):
with tempfile.TemporaryDirectory() as directory:
for option in ("--nid", "--ni"):
for json_output in (False, True):
with self.subTest(option=option, json_output=json_output):
result = subprocess.run(
[sys.executable, str(Path(nid_names.__file__)), option, "--db", directory,
*(["--json"] if json_output else [])], capture_output=True, text=True, timeout=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, "")
if json_output:
self.assertEqual(json.loads(result.stdout), {"db_entries": 0, "rows": []})
else:
self.assertIn("0 unknowns, 0 suggestions (0 names already in use)", result.stdout)
def test_environment_variable_selects_the_database_and_help_names_it(self):
script = str(Path(nid_names.__file__))
helped = subprocess.run([sys.executable, script, "--help"], capture_output=True, text=True, timeout=60)
self.assertEqual(helped.returncode, 0, helped.stderr)
self.assertIn("ANYPS5_NID_DB", " ".join(helped.stdout.split()))
with tempfile.TemporaryDirectory() as directory:
database = Path(directory) / "nid database.csv"
nid = nid_names.compute_nid("sceExample")
database.write_text(f"{nid} sceExample\n", encoding="utf-8")
scratch = str(Path(directory) / "scratch")
Path(scratch).mkdir()
tree = Path(directory) / "tree"
(tree / "core" / "libs" / "prx").mkdir(parents=True)
(tree / "tools").mkdir()
tool = tree / "tools" / "nid_names.py"
tool.write_bytes(Path(script).read_bytes())
environment = {**os.environ, "ANYPS5_NID_DB": str(database), "TEMP": scratch, "TMP": scratch, "TMPDIR": scratch}
result = subprocess.run([sys.executable, str(tool), "--nid", nid, "--json"], capture_output=True, text=True,
env=environment, timeout=20)
self.assertEqual(list(Path(scratch).iterdir()), [])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("downloading", result.stderr)
self.assertIn("sceExample", result.stdout)
def test_interrupted_download_does_not_publish_a_partial_cache(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "cache" / "aerolib.csv"
def interrupted(url, filename):
Path(filename).write_text("AAAAAAAAAAA scePartial\n", encoding="utf-8")
raise URLError("connection interrupted")
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=interrupted):
with self.assertRaises(URLError):
nid_names.load_db(cache)
self.assertFalse(cache.exists())
self.assertEqual(list(cache.parent.iterdir()), [])
def test_retry_after_interruption_downloads_the_complete_database(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
def interrupted(url, filename):
Path(filename).write_text("AAAAAAAAAAA scePartial\n", encoding="utf-8")
raise URLError("connection interrupted")
def complete(url, filename):
Path(filename).write_text("AAAAAAAAAAA sceComplete\nBBBBBBBBBBB sceOther\n", encoding="utf-8")
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=interrupted):
with self.assertRaises(URLError):
nid_names.load_db(cache)
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete) as download:
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceComplete", "BBBBBBBBBBB": "sceOther"})
download.assert_called_once()
self.assertEqual(list(cache.parent.iterdir()), [cache])
def test_cache_is_published_only_after_download_completes(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
def complete(url, filename):
self.assertEqual(url, nid_names.DB_URL)
self.assertNotEqual(Path(filename), cache)
self.assertEqual(Path(filename).parent, cache.parent)
self.assertFalse(cache.exists())
Path(filename).write_text("AAAAAAAAAAA sceComplete\n", encoding="utf-8")
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete):
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceComplete"})
self.assertEqual(cache.read_text(encoding="utf-8"), "AAAAAAAAAAA sceComplete\n")
self.assertEqual(list(cache.parent.iterdir()), [cache])
def test_existing_cache_is_used_without_downloading(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
cache.write_text("AAAAAAAAAAA sceExisting\n", encoding="utf-8")
with patch.object(nid_names.urllib.request, "urlretrieve") as download:
self.assertEqual(nid_names.load_db(cache), {"AAAAAAAAAAA": "sceExisting"})
download.assert_not_called()
def test_utf8_bom_does_not_change_the_first_nid(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
nid = nid_names.compute_nid("sceExample")
cache.write_text(f"{nid} sceExample\n", encoding="utf-8-sig")
self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"})
def test_comment_rows_do_not_become_database_entries(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
nid = nid_names.compute_nid("sceExample")
cache.write_text(f"# Database names\n{nid} sceExample\n", encoding="utf-8")
self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"})
def test_annotations_do_not_change_hash_inputs(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory) / "aerolib.csv"
nid = nid_names.compute_nid("sceExample")
cache.write_text(f"{nid}#A#B sceExample#verified\n", encoding="utf-8")
self.assertEqual(nid_names.load_db(cache), {nid: "sceExample"})
class NidArgumentTests(unittest.TestCase):
options = ["-h", "--help", "--db", "--nid", "--json"]
def test_name_and_nid_modes_conflict_even_with_empty_filters(self):
for name_option in ("--name", "--symbol", "--na", "--sym"):
for nid_option in ("--nid", "--ni"):
for names, nids in (
([name_option], [nid_option]),
([name_option, "ceilf"], [nid_option]),
([name_option + "=ceilf"], [nid_option]),
([name_option], [nid_option, "AAAAAAAAAAA"]),
([name_option], [nid_option + "=AAAAAAAAAAA"])):
for arguments in (names + nids, nids + names):
with self.subTest(arguments=arguments), \
contextlib.redirect_stdout(io.StringIO()) as output, \
contextlib.redirect_stderr(io.StringIO()) as error, \
patch.object(nid_names, "load_db", return_value={}) as database, \
patch.object(nid_names, "collect_unknowns", return_value={}) as sources, \
patch.object(nid_names, "collect_real_names", return_value=set()) as definitions:
with self.assertRaises(SystemExit) as exited:
nid_names.main([*arguments, "--json"])
self.assertEqual(exited.exception.code, 2)
self.assertIn("--name/--symbol cannot be combined with --nid", error.getvalue())
self.assertEqual(output.getvalue(), "")
database.assert_not_called()
sources.assert_not_called()
definitions.assert_not_called()
def test_invalid_nids_fail_before_database_or_source_access(self):
for nid in ("short", "ABCDEFGHIJKL", "ABCDEFGHIJ/", "ABCDEFGHIJ_", "ABCDEFGHIJ\u00e9", "", "--jsooon"):
with self.subTest(nid=nid):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as error, \
patch.object(nid_names, "load_db", return_value={}) as database, \
patch.object(nid_names, "collect_unknowns") as sources, \
patch.object(nid_names, "collect_real_names", return_value=set()), \
self.assertRaises(SystemExit) as exited:
nid_names.main(["--nid=" + nid, "--json"])
self.assertEqual(exited.exception.code, 2)
self.assertIn("invalid NID", error.getvalue())
database.assert_not_called()
sources.assert_not_called()
def test_option_typo_in_nid_list_is_not_an_unknown_success(self):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()) as error, \
patch.object(nid_names, "load_db", return_value={}) as database, \
patch.object(nid_names, "collect_real_names", return_value=set()), self.assertRaises(SystemExit) as exited:
nid_names.main(["--nid", "ZjtRqSMJwdw", "--jsooon"])
self.assertEqual(exited.exception.code, 2)
self.assertIn("--jsooon", error.getvalue())
database.assert_not_called()
def test_valid_unknown_nid_remains_a_successful_lookup(self):
output = io.StringIO()
with patch.object(nid_names, "load_db", return_value={}), \
patch.object(nid_names, "collect_real_names", return_value=set()), \
contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(["--nid=AAAAAAAAAA+", "--json"]), 0)
row, = json.loads(output.getvalue())["rows"]
self.assertEqual((row["nid"], row["status"]), ("AAAAAAAAAA+", "unknown"))
def test_nid_with_library_module_suffix_matches_the_bare_nid(self):
results = []
for nid in ("ZjtRqSMJwdw", "ZjtRqSMJwdw#C#D"):
output = io.StringIO()
with patch.object(nid_names, "load_db", return_value={"ZjtRqSMJwdw": "sinh"}), \
patch.object(nid_names, "collect_real_names", return_value=set()), \
contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(["--nid=" + nid, "--json"]), 0)
results.append(json.loads(output.getvalue())["rows"])
self.assertEqual(results[0], results[1])
self.assertEqual(results[1][0]["nid"], "ZjtRqSMJwdw")
def normalize(self, arguments):
return nid_names.nid_arguments(arguments, self.options)
def test_nids_starting_with_a_dash_are_accepted(self):
self.assertEqual(
self.normalize(["--nid", "-nvxBWa0iDs", "ZjtRqSMJwdw", "--ll-AgiK2M", "--json"]),
["--nid=-nvxBWa0iDs", "--nid=ZjtRqSMJwdw", "--nid=--ll-AgiK2M", "--json"])
def test_nid_with_value_after_a_bare_nid_is_kept(self):
self.assertEqual(self.normalize(["--nid", "A", "--nid=B"]), ["--nid=A", "--nid=B"])
def test_abbreviated_options_end_the_nids(self):
self.assertEqual(self.normalize(["--nid", "A", "--j"]), ["--nid=A", "--j"])
self.assertEqual(self.normalize(["--nid", "A", "--d=x.csv"]), ["--nid=A", "--d=x.csv"])
self.assertEqual(self.normalize(["--ni", "-nvxBWa0iDs"]), ["--nid=-nvxBWa0iDs"])
def test_end_of_options_marker_is_kept(self):
self.assertEqual(self.normalize(["--nid", "A", "--", "B"]), ["--nid=A", "--", "B"])
def test_main_resolves_every_nid_given_after_nid(self):
database = {"-nvxBWa0iDs": "gethostname", "ZjtRqSMJwdw": "sinh"}
arguments = ["nid_names.py", "--json", "--nid", "-nvxBWa0iDs", "ZjtRqSMJwdw"]
with (patch.object(sys, "argv", arguments),
patch.object(nid_names, "load_db", return_value=database),
patch.object(nid_names, "collect_real_names", return_value=set()),
patch("builtins.print") as printed):
self.assertEqual(nid_names.main(), 0)
rows = json.loads(printed.call_args.args[0])["rows"]
self.assertEqual({row["nid"]: row["suggestion"] for row in rows}, database)
def test_symbol_files_and_stdin_compute_nids_without_a_database(self):
with tempfile.TemporaryDirectory() as directory:
names = Path(directory) / "symbols.txt"
symbols = ["sceExample", "-leading-dash", " literal spaces ", "unicode_Ω", "sceExample"]
names.write_bytes(("\ufeff" + "\r\n".join(symbols) + "\r\n\r\n").encode("utf-8"))
for source in (str(names), "-"):
with self.subTest(source=source), patch("sys.stdin", io.StringIO(names.read_text(encoding="utf-8-sig"))), \
patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \
contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(nid_names.main(["--name", "first", "--from-file", source, "--json"]), 0)
rows = json.loads(output.getvalue())["symbols"]
self.assertEqual(rows, [{"name": name, "nid": nid_names.compute_nid(name)} for name in ["first", *symbols]])
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited:
nid_names.main(["--from-file", str(names), "--nid"])
self.assertEqual(exited.exception.code, 2)
with patch.object(nid_names, "load_db", side_effect=AssertionError("database used")), \
contextlib.redirect_stdout(io.StringIO()) as output, \
contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as exited:
nid_names.main(["--from-file", str(names.parent / "missing"), "--json"])
self.assertEqual(exited.exception.code, 2)
self.assertEqual(output.getvalue(), "")
class SourceCollectionTests(unittest.TestCase):
def test_trailing_return_definitions_are_collected_without_declarations_or_literals(self):
text = ('auto APS5_VABI sceTrailing() -> int { return 0; }\n'
'auto APS5_VABI sceNoThrow() noexcept(true) -> unsigned long { return 0; }\n'
'auto APS5_VABI pointer_nid_postfix() -> const char* { return nullptr; }\n'
'auto APS5_VABI sceDeclaration() -> int;\n'
'const char* example = "auto APS5_VABI sceQuoted() -> int { return 0; }";\n'
'/* auto APS5_VABI sceComment() -> int { return 0; } */\n')
self.write_source(text)
expected = {"sceTrailing", "sceNoThrow", "pointer", "pointer_nid_postfix"}
self.assertEqual(nid_names.collect_real_names(), expected)
matches = list(nid_names.source_definitions(text))
self.assertEqual([match.group(1) for match in matches], ["sceTrailing", "sceNoThrow", "pointer_nid_postfix"])
self.assertTrue(all(text[match.end() - 1] == "{" for match in matches))
def test_trailing_return_export_names_reach_the_overlap_consumer(self):
import pr_overlap
text = "auto APS5_VABI sceTrailing() noexcept(false) -> int { return 0; }\n"
exports = pr_overlap.source_exports(text, "core/libs/prx/libSceExample")
self.assertEqual(exports, {("core/libs/prx/libSceExample", nid_names.compute_nid("sceTrailing")): {"sceTrailing"}})
def setUp(self):
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.root = Path(self.directory.name)
self.prx = self.root / "core" / "libs" / "prx"
self.module = self.prx / "libSceExample"
self.module.mkdir(parents=True)
self.root_patch = patch.object(nid_names, "ROOT", self.root)
self.prx_patch = patch.object(nid_names, "PRX", self.prx)
self.root_patch.start()
self.prx_patch.start()
self.addCleanup(self.root_patch.stop)
self.addCleanup(self.prx_patch.stop)
def write_source(self, text, relative="Export.cpp"):
path = self.module / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def cli(self, *arguments):
tools = self.root / "tools"
tools.mkdir(exist_ok=True)
tool = tools / "nid_names.py"
tool.write_bytes(Path(nid_names.__file__).read_bytes())
return subprocess.run([sys.executable, str(tool), *map(str, arguments)],
env={**os.environ, "PYTHONIOENCODING": "cp1252"},
capture_output=True, timeout=10)
def test_report_preserves_unicode_paths_with_legacy_stdout(self):
self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n', "\u8cc7\u6599.cpp")
database = self.root / "aerolib.csv"
database.write_bytes(b"")
result = self.cli("--db", database)
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace"))
self.assertIn("core/libs/prx/libSceExample/\u8cc7\u6599.cpp", result.stdout.decode("utf-8"))
def test_named_stub_and_implementation_are_reported_as_names_in_use(self):
stub_name, implementation_name = "scePadPending", "scePadImplemented"
stub_nid = nid_names.compute_nid(stub_name)
implementation_nid = nid_names.compute_nid(implementation_name)
self.write_source(
f'APS5_EXPORT("{stub_nid}", exampleUnknown00);\n'
f'APS5_EXPORT("{implementation_nid}", exampleUnknown01);\n'
'int APS5_VABI scePadPending() { NotImplemented_nid_no_patch(__func__); return 0; }\n'
'int APS5_VABI scePadImplemented() { return 1; }\n')
database = self.root / "aerolib.csv"
database.write_text(f"{stub_nid} {stub_name}\n{implementation_nid} {implementation_name}\n", encoding="utf-8")
text_result = self.cli("--db", database)
self.assertEqual(text_result.returncode, 0, text_result.stderr.decode("utf-8", errors="replace"))
text = text_result.stdout.decode("utf-8")
self.assertIn(f"{stub_nid} ", text)
self.assertIn(f"{implementation_nid} ", text)
self.assertIn("name-in-use scePadPending", text)
self.assertIn("name-in-use scePadImplemented", text)
self.assertEqual(text.count("name-in-use"), 2)
self.assertIn("2 names already in use", text)
json_result = self.cli("--db", database, "--json")
self.assertEqual(json_result.returncode, 0, json_result.stderr.decode("utf-8", errors="replace"))
rows = json.loads(json_result.stdout.decode("utf-8"))["rows"]
self.assertEqual({row["nid"]: row["status"] for row in rows},
{stub_nid: "name-in-use", implementation_nid: "name-in-use"})
def test_error_preserves_unicode_paths_with_legacy_stderr(self):
database = self.root / "\u8cc7\u6599"
database.mkdir()
result = self.cli("--db", database)
self.assertEqual(result.returncode, 2, result.stderr.decode("utf-8", errors="replace"))
self.assertIn("\u8cc7\u6599", result.stderr.decode("utf-8"))
self.assertNotIn(b"Traceback", result.stderr)
def test_comments_and_literals_do_not_create_export_records(self):
self.write_source('''// APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);
/* int APS5_VABI sceCommented() { return 0; } */
auto example = R"text(APS5_EXPORT("BBBBBBBBBBB", exampleUnknown01);
int APS5_VABI sceQuoted() { return 0; })text";
APS5_EXPORT("CCCCCCCCCCC", exampleUnknown02);
int APS5_VABI sceReal() { return 0; }
''')
self.assertEqual(nid_names.collect_unknowns(), {"CCCCCCCCCCC": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown02"})
self.assertEqual(nid_names.collect_real_names(), {"sceReal"})
def test_assembly_export_macros_are_real_names(self):
self.write_source('''asm(".text\n"
APS5_ASM_FUNCTION("sceAsmCall_nid_postfix")
"ret\n"
APS5_SIGNAL_ASM_FUNCTION("sigAsmCall_nid_postfix")
"ret\n");
// APS5_ASM_FUNCTION("sceCommented_nid_postfix")
auto example = "APS5_ASM_FUNCTION(\"sceQuoted_nid_postfix\")";
''')
self.assertEqual(nid_names.collect_real_names(), {
"sceAsmCall", "sceAsmCall_nid_postfix",
"sigAsmCall", "sigAsmCall_nid_postfix",
})
def test_disambiguated_definitions_are_real_names_of_their_symbol(self):
self.write_source('int APS5_VABI fstat_nid_disambig1_nid_postfix(int fd) { return 0; }\n'
'int APS5_VABI pwrite_nid_disambig12(int fd) { return 0; }\n'
'int APS5_VABI sceKeep_nid_disambigA() { return 0; }\n')
self.assertEqual(nid_names.collect_real_names(), {
"fstat", "fstat_nid_disambig1_nid_postfix",
"pwrite", "pwrite_nid_disambig12",
"sceKeep_nid_disambigA",
})
def test_formatted_alias_macro_is_detected(self):
self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n')
self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"})
def test_known_aliases_are_not_unknown_exports(self):
self.write_source('APS5_EXPORT("AAAAAAAAAAA", sceKnownCall);\n'
'APS5_EXPORT("BBBBBBBBBBB", exampleUnknown00);\n')
self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"})
def test_windows_source_paths_use_portable_separators(self):
self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n')
relative = PureWindowsPath("core/libs/prx/libSceExample/Export.cpp")
with patch.object(Path, "relative_to", return_value=relative):
self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"})
def test_test_fixtures_are_not_runtime_implementations(self):
self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nint APS5_VABI sceFixture() { return 0; }\n', "tests/Fixture.cpp")
self.assertEqual(nid_names.collect_unknowns(), {})
self.assertEqual(nid_names.collect_real_names(), set())
def test_unreadable_sources_fail_instead_of_returning_incomplete_names(self):
self.write_source('int APS5_VABI sceReal() { return 0; }\n')
with patch.object(Path, "read_text", side_effect=OSError("read denied")):
with self.assertRaisesRegex(OSError, "read denied"):
nid_names.collect_unknowns()
with self.assertRaisesRegex(OSError, "read denied"):
nid_names.collect_real_names()
def test_explicit_empty_nid_filter_does_not_scan_the_tree(self):
cache = self.root / "aerolib.csv"
cache.write_text("AAAAAAAAAAA example\n", encoding="utf-8")
output = io.StringIO()
with patch.object(sys, "argv", ["nid_names.py", "--db", str(cache), "--nid", "--json"]), patch.object(nid_names, "collect_unknowns", side_effect=AssertionError("unexpected tree scan")), contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(), 0)
self.assertEqual(json.loads(output.getvalue())["rows"], [])
def test_empty_abbreviated_filter_skips_all_source_scans(self):
cache = self.root / "aerolib.csv"
cache.write_text("AAAAAAAAAAA example\n", encoding="utf-8")
arguments = ["nid_names.py", "--db", str(cache), "--ni", "--json"]
with patch.object(sys, "argv", arguments), patch.object(nid_names, "collect_unknowns", side_effect=AssertionError("unexpected aliases scan")), patch.object(nid_names, "collect_real_names", side_effect=AssertionError("unexpected definitions scan")), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(nid_names.main(), 0)
def test_literals_and_digit_separators_keep_following_definitions(self):
self.write_source('''auto count = 1'000;
auto quote = '\\'';
auto string = "\\\" APS5_VABI sceQuoted() { }";
int APS5_VABI sceReal() { return 0; }
''')
self.assertEqual(nid_names.collect_real_names(), {"sceReal"})
def test_name_option_computes_symbol_nid(self):
output = io.StringIO()
with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "floorf"]), contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(), 0)
text = output.getvalue()
self.assertIn("ceilf", text)
self.assertIn("GAUuLKGhsCw", text)
self.assertIn("floorf", text)
self.assertIn("mKhVDmYciWA", text)
def test_name_option_with_json_emits_symbols_list(self):
output = io.StringIO()
with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "--json"]), contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(), 0)
data = json.loads(output.getvalue())
self.assertEqual(data, {"symbols": [{"name": "ceilf", "nid": "GAUuLKGhsCw"}]})
def test_symbol_alias_does_not_load_database(self):
output = io.StringIO()
with patch.object(sys, "argv", ["nid_names.py", "--symbol", "truncf"]), \
patch.object(nid_names, "load_db", side_effect=AssertionError("unexpected db load")), \
contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(), 0)
self.assertIn("Vo8rvWtZw3g", output.getvalue())
def test_name_option_rejects_nid_and_keeps_long_names(self):
with patch.object(sys, "argv", ["nid_names.py", "--name", "ceilf", "--nid", "AAAAAAAAAAA"]), \
contextlib.redirect_stderr(io.StringIO()):
with self.assertRaises(SystemExit):
nid_names.main()
long_name = "a" * 50
output = io.StringIO()
with patch.object(sys, "argv", ["nid_names.py", "--name", long_name]), contextlib.redirect_stdout(output):
self.assertEqual(nid_names.main(), 0)
self.assertIn(long_name, output.getvalue())
def test_spliced_line_comment_and_macro_tokens(self):
self.write_source('// continued \\\nAPS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\nAPS5_EX\\\nPORT("BBBBBBBBBBB", exampleUnknown01);\n')
self.assertEqual(nid_names.collect_unknowns(), {"BBBBBBBBBBB": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown01"})
def test_main_reconfigures_streams(self):
class Stream:
def __init__(self):
self.encoding = None
self.errors = None
def reconfigure(self, encoding=None, errors=None):
self.encoding = encoding
self.errors = errors
def write(self, text):
pass
def flush(self):
pass
out, err = Stream(), Stream()
with patch.object(nid_names.sys, "stdout", out), patch.object(nid_names.sys, "stderr", err):
try:
nid_names.main(["--invalid-argument"])
except SystemExit:
pass
self.assertEqual(out.encoding, "utf-8")
self.assertEqual(out.errors, "backslashreplace")
self.assertEqual(err.encoding, "utf-8")
self.assertEqual(err.errors, "backslashreplace")
if __name__ == "__main__":
unittest.main()