mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
353 lines
19 KiB
Python
353 lines
19 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import sys
|
|
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):
|
|
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("nid1 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("nid1 scePartial\n", encoding="utf-8")
|
|
raise URLError("connection interrupted")
|
|
|
|
def complete(url, filename):
|
|
Path(filename).write_text("nid1 sceComplete\nnid2 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), {"nid1": "sceComplete", "nid2": "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("nid1 sceComplete\n", encoding="utf-8")
|
|
|
|
with patch.object(nid_names.urllib.request, "urlretrieve", side_effect=complete):
|
|
self.assertEqual(nid_names.load_db(cache), {"nid1": "sceComplete"})
|
|
self.assertEqual(cache.read_text(encoding="utf-8"), "nid1 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("nid1 sceExisting\n", encoding="utf-8")
|
|
with patch.object(nid_names.urllib.request, "urlretrieve") as download:
|
|
self.assertEqual(nid_names.load_db(cache), {"nid1": "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 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)
|
|
|
|
|
|
class SourceCollectionTests(unittest.TestCase):
|
|
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_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_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"})
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|