AnyPS5/tools/tests/test_nid_names.py

218 lines
11 KiB
Python

import contextlib
import io
import json
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 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 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_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_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()