import os import shutil import subprocess import tempfile import unittest from pathlib import Path SCRIPT = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-seed-redists" GAME_SCRIPT = r'''"InstallScript" { "Run Process" { "VCRedist2022" { "HasRunKey" "HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\Apps\\CommonRedist\\vcredist\\2022" "process 1" "%INSTALLDIR%\\_CommonRedist\\vcredist\\2022\\VC_redist.x64.exe" "command 1" "/q /norestart" "NoCleanUp" "1" } } } ''' VULKAN_SCRIPT = r'''"InstallScript" { "Run Process" { "Vulkan111080" { "HasRunKey" "HKEY_LOCAL_MACHINE\\Software\\Valve\\Steam\\Apps\\1174180" "process 1" "%INSTALLDIR%\\Redistributables\\VulkanRT-1.1.108.0-Installer.exe" "command 1" "/S" "NoCleanUp" "0" } "RockstarLauncher" { "HasRunKey" "HKEY_LOCAL_MACHINE\\Software\\Rockstar Games\\Steam\\Launcher" "process 1" "%INSTALLDIR%\\Redistributables\\Rockstar-Games-Launcher.exe" "command 1" "/s /t" "NoCleanUp" "0" } } } ''' UBISOFT_SCRIPT = r'''"installscript" { "Run Process" { "Ubisoft Connect PC Client" { "process 1" "%INSTALLDIR%\\UbisoftConnectInstaller.exe" "command 1" "/S" } "easyanticheat" { "HasRunKey" "HKEY_CURRENT_USER\\SOFTWARE\\Ubisoft\\WatchDogs2EAC" "process 1" "%INSTALLDIR%\\EasyAntiCheat\\InstallWD2EAC.cmd" "command 1" "" } } } ''' EA_SCRIPT = r'''"installscript" { "Run Process" { "EADesktopSetup" { "process 1" "%INSTALLDIR%\\__Installer\\Origin\\redist\\internal\\EAappInstaller.exe" "command 1" "EAX_LAUNCH_CLIENT=0 IGNORE_INSTALLED=1" "HasRunStringKey" "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Desktop\\InstallSuccessful" "HasRunStringValue" "true" "RunType" "1" } "Windows6.1-KB2670838-x64.msu" { "process 1" "%INSTALLDIR%\\__Installer\\customcomponent\\patch_KB2670838_x64\\Windows6.1-KB2670838-x64.msu" "command 1" "/quiet" "Requirement_OS" { "OSType" "Windows 7" } } } } ''' class SeedRedistsTest(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, self.tmp, True) self.steam = self.tmp / "Steam" self.reg = self.steam / "steamapps/compatdata/1234/pfx/system.reg" self.reg.parent.mkdir(parents=True) self.reg.write_text("WINE REGISTRY Version 2\n") self.user = self.reg.with_name("user.reg") self.user.write_text('WINE REGISTRY Version 2\n\n[Software\\\\Unrelated] 1\n"Keep"="untouched"\n') def game(self, name="Some Game", script=GAME_SCRIPT, library=None): root = Path(library or self.steam) path = root / "steamapps/common" / name / "_CommonRedist/vcredist/2022/installscript.vdf" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(script) manifest = root / "steamapps" / ("appmanifest_%d.acf" % (abs(hash(name)) % 100000)) manifest.write_text('"AppState" { "installdir" "%s" }' % name) return path def run_seed(self, libraries=""): env = dict(os.environ, DROIDDECK_LIBRARIES=libraries) return subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) def test_a_games_own_install_script_is_marked_as_run(self): self.game() self.run_seed() reg = self.reg.read_text() self.assertIn("[Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022]", reg) self.assertIn("[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022]", reg) self.assertIn('"VCRedist2022"=dword:00000001', reg) def test_games_in_other_libraries_are_read(self): sd = self.tmp / "sd" self.game(library=sd) self.run_seed(str(sd)) self.assertIn('"VCRedist2022"=dword:00000001', self.reg.read_text()) def test_current_user_and_machine_markers_go_to_their_own_registries(self): self.game(script=GAME_SCRIPT + GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER").replace("VCRedist2022", "UserRedist")) with self.reg.open("a") as out: out.write('\n[Software\\\\Unrelated] 1\n"Keep"="machine"\n') for reg in (self.reg, self.user): os.utime(reg, ns=(1_000_000_000_123456789, 1_000_000_000_123456789)) self.run_seed() system, user = self.reg.read_text(), self.user.read_text() self.assertEqual(system.count('"VCRedist2022"=dword:00000001'), 2) self.assertNotIn("UserRedist", system) self.assertEqual(user.count('"UserRedist"=dword:00000001'), 1) self.assertNotIn("Wow6432Node", user) self.assertNotIn("VCRedist2022", user) self.assertIn('"Keep"="machine"', system) self.assertIn('"Keep"="untouched"', user) self.run_seed() self.assertEqual(system, self.reg.read_text()) self.assertEqual(user, self.user.read_text()) for reg in (self.reg, self.user): self.assertEqual(reg.stat().st_mtime_ns, 1_000_000_000_123456789) def test_a_user_marker_with_the_same_machine_path_and_value_is_separate(self): self.game(script=GAME_SCRIPT + GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER")) self.run_seed() system = self.reg.read_text() self.assertEqual(self.user.read_text().count('"VCRedist2022"=dword:00000001'), 1) self.user.write_text("WINE REGISTRY Version 2\n") self.run_seed() self.assertEqual(self.user.read_text().count('"VCRedist2022"=dword:00000001'), 1) self.assertEqual(system, self.reg.read_text()) def test_a_legacy_user_ledger_entry_is_checked_in_user_reg(self): self.game(script=GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER")) self.run_seed() system = self.reg.read_bytes() self.user.write_text("WINE REGISTRY Version 2\n") (self.reg.parent / ".droiddeck-redists-checked").write_text(subprocess.check_output(["stat", "-c", "%i %s %.9Y", str(self.reg)], text=True)) self.run_seed() self.assertEqual(self.user.read_text().count('"VCRedist2022"=dword:00000001'), 1) self.assertEqual(system, self.reg.read_bytes()) def test_an_absent_user_registry_is_not_created_or_claimed(self): self.game(script=GAME_SCRIPT + GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER").replace("VCRedist2022", "UserRedist")) self.user.unlink() self.run_seed() self.assertFalse(self.user.exists()) self.assertNotIn("UserRedist", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(self.reg.read_text().count('"VCRedist2022"=dword:00000001'), 2) system = self.reg.read_bytes() self.user.write_text("WINE REGISTRY Version 2\n") self.run_seed() self.assertIn('"UserRedist"=dword:00000001', self.user.read_text()) self.run_seed() self.user.unlink() self.run_seed() self.assertFalse(self.user.exists()) self.assertNotIn("UserRedist", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(system, self.reg.read_bytes()) @unittest.skipIf(os.name == "posix" and os.geteuid() == 0, "root can write through read-only mode bits") def test_an_unwritable_user_registry_does_not_claim_a_missing_marker(self): self.game(script=GAME_SCRIPT + GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER").replace("VCRedist2022", "UserRedist")) ledger = self.reg.parent / ".droiddeck-redists" ledger.write_text("HKEY_CURRENT_USER\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022\tUserRedist\n") user = self.user.read_bytes() self.user.chmod(0o444) try: self.run_seed() self.assertEqual(user, self.user.read_bytes()) self.assertEqual(self.user.stat().st_mode & 0o777, 0o444) self.assertNotIn("UserRedist", ledger.read_text()) self.assertEqual(self.reg.read_text().count('"VCRedist2022"=dword:00000001'), 2) finally: self.user.chmod(0o644) def test_user_markers_in_a_default_prefix_and_another_library_are_seeded(self): default = self.steam / "steamapps/common/Proton Test/files/share/default_pfx" default.mkdir(parents=True) for name in ("system.reg", "user.reg"): (default / name).write_text("WINE REGISTRY Version 2\n") sd = self.tmp / "sd" self.game(library=sd, script=GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER")) self.run_seed(str(sd)) self.assertIn('"VCRedist2022"=dword:00000001', (default / "user.reg").read_text()) self.assertNotIn('"VCRedist2022"', (default / "system.reg").read_text()) self.assertIn('"VCRedist2022"=dword:00000001', self.user.read_text()) def test_the_vc_runtime_is_recorded_for_every_architecture_and_view(self): self.run_seed() reg = self.reg.read_text() for view in ("Software", "Software\\\\Wow6432Node"): for arch in ("x86", "x64", "arm64"): self.assertIn("[%s\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\%s]" % (view, arch), reg) self.assertIn('"Installed"=dword:00000001', reg) self.assertIn('"Major"=dword:0000000e', reg) self.assertIn('"Version"="v14.51.36247.00"', reg) def test_a_seeded_prefix_is_left_alone_until_a_game_changes(self): self.run_seed() once = self.reg.read_text() self.run_seed() self.assertEqual(once, self.reg.read_text()) self.game() self.run_seed() self.assertIn('"VCRedist2022"=dword:00000001', self.reg.read_text()) def test_markers_a_replaced_registry_lost_are_seeded_again(self): self.game() stock = self.reg.read_text() self.run_seed() ledger = (self.reg.parent / ".droiddeck-redists").read_text() self.assertIn("VCRedist2022", ledger) self.reg.write_text(stock) self.run_seed() reg = self.reg.read_text() self.assertEqual(reg.count('"VCRedist2022"=dword:00000001'), 2, "both views again") self.assertIn("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]", reg) self.assertEqual(sorted((self.reg.parent / ".droiddeck-redists").read_text().splitlines()), sorted(ledger.splitlines())) self.run_seed() self.assertEqual(reg, self.reg.read_text()) def test_a_marker_kept_in_only_one_view_is_seeded_again(self): self.game() self.run_seed() wow = "[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022]" blocks = self.reg.read_text().split("\n\n") self.reg.write_text("\n\n".join(b for b in blocks if not (b.startswith(wow) and '"VCRedist2022"' in b))) self.assertEqual(self.reg.read_text().count('"VCRedist2022"'), 1) self.run_seed() reg = self.reg.read_text() self.assertEqual(reg.count('"VCRedist2022"'), 3, "the one left plus both views again") self.assertTrue(any(b.startswith(wow) and '"VCRedist2022"' in b for b in reg.split("\n\n"))) def test_a_vc_runtime_another_architecture_lost_is_seeded_again(self): self.run_seed() arm64 = "[Software\\\\Wow6432Node\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\arm64]" blocks = self.reg.read_text().split("\n\n") self.reg.write_text("\n\n".join(b for b in blocks if not b.startswith(arm64))) self.assertNotIn(arm64, self.reg.read_text()) self.run_seed() reg = self.reg.read_text() self.assertIn(arm64, reg) self.assertEqual(reg.count("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]"), 2) def test_a_marker_written_in_one_view_only_is_kept_as_it_is(self): self.game(script=GAME_SCRIPT.replace("Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022", "SOFTWARE\\\\AGEIA Technologies").replace("VCRedist2022", "PhysX Version")) self.run_seed() once = self.reg.read_text() self.assertEqual(once.count('"PhysX Version"=dword:00000001'), 1) result = self.run_seed() self.assertEqual(once, self.reg.read_text()) self.assertNotIn("gone", result.stdout) def test_an_evaluator_script_nested_deeper_is_read(self): script = self.tmp / "legacycompat/evaluatorscript_409710.vdf" script.parent.mkdir(parents=True) script.write_text('"evaluatorscript"\n{\n\t"1"\n' + "\n".join("\t" + line for line in GAME_SCRIPT.replace("VCRedist2022", "x64 14.99.1.0").splitlines()) + "\n}\n") env = dict(os.environ, DROIDDECK_LIBRARIES="", DROIDDECK_SEED_SCRIPTS=str(script)) subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) self.assertIn('"x64 14.99.1.0"=dword:00000001', self.reg.read_text()) def test_an_unchanged_registry_is_not_read_again(self): self.game(script=GAME_SCRIPT + GAME_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER").replace("VCRedist2022", "UserRedist")) self.run_seed() self.run_seed() shim = self.tmp / "bin" shim.mkdir() log = self.tmp / "awk.log" (shim / "awk").write_text('#!/bin/sh\nprintf "%%s\\n" "$*" >> "%s"\nexec %s "$@"\n' % (log, shutil.which("awk"))) (shim / "awk").chmod(0o755) env = dict(os.environ, DROIDDECK_LIBRARIES="", PATH="%s:%s" % (shim, os.environ["PATH"])) once = self.reg.read_text() user = self.user.read_text() subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) self.assertEqual(once, self.reg.read_text()) self.assertEqual(user, self.user.read_text()) self.assertNotIn(str(self.reg), log.read_text() if log.exists() else "") self.assertNotIn(str(self.user), log.read_text() if log.exists() else "") replaced = self.user.with_name("user.reg.new") replaced.write_text(user.replace('"UserRedist"', '"OtherValue"')) os.utime(replaced, ns=(self.user.stat().st_atime_ns, self.user.stat().st_mtime_ns)) self.assertEqual(replaced.stat().st_size, self.user.stat().st_size) os.replace(replaced, self.user) subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) self.assertEqual(self.user.read_text().count('"UserRedist"=dword:00000001'), 1) self.assertIn('"OtherValue"=dword:00000001', self.user.read_text()) self.assertEqual(once, self.reg.read_text()) replaced = self.reg.with_name("system.reg.new") replaced.write_text("WINE REGISTRY Version 2\n") os.replace(replaced, self.reg) subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) self.assertEqual(self.reg.read_text().count('"VCRedist2022"=dword:00000001'), 2) def test_the_prefix_date_is_kept(self): os.utime(self.reg, (1_000_000_000, 1_000_000_000)) self.run_seed() self.assertEqual(int(self.reg.stat().st_mtime), 1_000_000_000) def rdr2(self): path = self.steam / "steamapps/common/Red Dead Redemption 2/installscript.vdf" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(VULKAN_SCRIPT) (self.steam / "steamapps/appmanifest_1174180.acf").write_text('"AppState" { "installdir" "Red Dead Redemption 2" }') def test_a_vulkan_runtime_installer_is_left_to_run(self): self.rdr2() self.run_seed() reg = self.reg.read_text() self.assertNotIn("Vulkan111080", reg) self.assertIn('"RockstarLauncher"=dword:00000001', reg) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) def test_a_current_user_vulkan_installer_is_left_to_run(self): self.game(script=VULKAN_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER")) self.run_seed() self.assertIn('"RockstarLauncher"=dword:00000001', self.user.read_text()) self.assertNotIn("Vulkan111080", self.user.read_text()) self.assertNotIn("Vulkan111080", self.reg.read_text()) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) def test_a_current_user_vulkan_marker_comes_out_of_only_its_own_hive(self): self.game(script=VULKAN_SCRIPT.replace("HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER")) self.run_seed() system = self.reg.read_bytes() + b'\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1\n"Vulkan111080"=dword:00000001\n' self.reg.write_bytes(system) with self.user.open("a") as out: out.write('\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1\n"Other"="keep"\n"Vulkan111080"=dword:00000001\n' '\n[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1\n"Vulkan111080"=dword:00000001\n' '\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\22380] 1\n"Vulkan111080"=dword:00000001\n') with (self.reg.parent / ".droiddeck-redists").open("a") as out: out.write("HKEY_CURRENT_USER\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180\tVulkan111080\n") os.utime(self.user, ns=(1_000_000_000_123456789, 1_000_000_000_123456789)) self.run_seed() user = self.user.read_text() self.assertEqual(user.count('"Vulkan111080"'), 2, "the other path and the unseeded user view stay") self.assertIn('"Other"="keep"', user) self.assertEqual(system, self.reg.read_bytes()) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(self.user.stat().st_mtime_ns, 1_000_000_000_123456789) with self.user.open("a") as out: out.write('\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1\n"Vulkan111080"=dword:00000001\n') user = self.user.read_bytes() self.run_seed() self.assertEqual(user, self.user.read_bytes()) def test_a_vulkan_marker_an_earlier_seeding_wrote_comes_out_once(self): self.rdr2() marker = "HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180\tVulkan111080" self.reg.write_text( "WINE REGISTRY Version 2\n" "\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1791729070\n#time=1dd598d29161f72\n" '"Other"=dword:00000001\n"Vulkan111080"=dword:00000001\n' "\n[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1791729070\n" '"Vulkan111080"=dword:00000001\n' "\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\22380] 1791311644\n" '"Vulkan111080"=dword:00000001\n') (self.reg.parent / ".droiddeck-redists").write_text(marker + "\n") os.utime(self.reg, (1_000_000_000, 1_000_000_000)) self.run_seed() reg = self.reg.read_text() self.assertEqual(reg.count('"Vulkan111080"'), 1, "only the other app's value stays") self.assertIn('"Other"=dword:00000001', reg) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(int(self.reg.stat().st_mtime), 1_000_000_000) with self.reg.open("a") as out: out.write('\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1791730000\n"Vulkan111080"=dword:00000001\n') self.run_seed() self.assertEqual(self.reg.read_text().count('"Vulkan111080"'), 2) def evaluator(self, appid, install_script): script = self.tmp / ("legacycompat/evaluatorscript_%s.vdf" % appid) script.parent.mkdir(parents=True) script.write_text('"evaluatorscript"\n{\n\t"appid"\t\t"%s"\n\t"1"\n' % appid + "\n".join("\t" + line for line in install_script.splitlines()) + "\n}\n") env = dict(os.environ, DROIDDECK_LIBRARIES="", DROIDDECK_SEED_SCRIPTS=str(script)) return subprocess.run(["bash", str(SCRIPT), str(self.steam)], env=env, capture_output=True, text=True, check=True) def ubisoft(self): return self.evaluator("447040", UBISOFT_SCRIPT) def test_the_ubisoft_connect_installer_is_left_to_run(self): self.ubisoft() self.assertIn('"easyanticheat"=dword:00000001', self.user.read_text()) self.assertNotIn("Ubisoft Connect PC Client", self.reg.read_text()) self.assertNotIn("Ubisoft Connect PC Client", (self.reg.parent / ".droiddeck-redists").read_text()) def test_a_ubisoft_connect_marker_an_earlier_seeding_wrote_comes_out(self): marker = "HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\447040\tUbisoft Connect PC Client" self.reg.write_text( "WINE REGISTRY Version 2\n" "\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\447040] 1791729070\n" '"Ubisoft Connect PC Client"=dword:00000001\n' "\n[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\447040] 1791729070\n" '"Ubisoft Connect PC Client"=dword:00000001\n') (self.reg.parent / ".droiddeck-redists").write_text(marker + "\n") result = self.ubisoft() self.assertNotIn("Ubisoft Connect PC Client", self.reg.read_text()) self.assertNotIn("Ubisoft Connect PC Client", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertIn("Ubisoft Connect and EA app markers out", result.stdout) def test_the_ea_app_installer_is_left_to_run(self): self.evaluator("1238080", EA_SCRIPT) reg = self.reg.read_text() self.assertNotIn("EADesktopSetup", reg) self.assertEqual(reg.count('"Windows6.1-KB2670838-x64.msu"=dword:00000001'), 2) self.assertNotIn("EADesktopSetup", (self.reg.parent / ".droiddeck-redists").read_text()) def test_an_ea_app_marker_an_earlier_seeding_wrote_comes_out(self): marker = "HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\1238080\tEADesktopSetup" self.reg.write_text( "WINE REGISTRY Version 2\n" "\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1238080] 1793680524\n" '"EADesktopSetup"=dword:00000001\n' "\n[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\1238080] 1793680524\n" '"EADesktopSetup"=dword:00000001\n') (self.reg.parent / ".droiddeck-redists").write_text(marker + "\n") result = self.evaluator("1238080", EA_SCRIPT) self.assertNotIn("EADesktopSetup", self.reg.read_text()) self.assertNotIn("EADesktopSetup", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertIn("EA app markers out", result.stdout) if __name__ == "__main__": unittest.main()