"""The `emulator` runtime profile: a `script` component on the android toolchain that needs an emulator, selected explicitly and unqualified until the operators record it. UNTESTED: written while test execution was suspended. Source-only; no process, no network. A fake catalog proves the plan shape when an operator HAS recorded an image and a pool label; the real shipped catalog proves the profile is blocked by name today. Nothing here claims an emulator, KVM or a runner capability exists. """ import unittest from typing import Any from platform_ci.build_tools import BUILD_TOOLS, EMULATOR_PROFILE, POOL_EXECUTION, PROFILE_CONTAINER_OPTIONS from platform_ci.configuration import ConfigurationError, parse_project_configuration from platform_ci.plan import Plan, build_matrix, pool_container_options from platform_ci.runtimes import RuntimeCatalog, RuntimeCatalogError, load_runtime_catalog, parse_runtime_catalog from tests.support import event, plan_for ANDROID_IMAGE = f"registry.example/android@sha256:{'5' * 64}" EMULATOR_IMAGE = f"registry.example/android-emulator@sha256:{'6' * 64}" JAVA_IMAGE = f"registry.example/java@sha256:{'7' * 64}" POOL = "test-emulator-pool" def catalog(*, emulator_image: str | None = EMULATOR_IMAGE, pool: str | None = POOL) -> RuntimeCatalog: """A fake catalog in which an operator has (or has not) recorded the emulator image and pool label.""" runners: dict[str, str | None] = {"emulator": pool} if pool is not None else {} return parse_runtime_catalog( { "schema": 1, "runners": {"linux-amd64": runners}, "toolchains": {"android": {"2026.10.1": {"image": ANDROID_IMAGE, "emulator_image": emulator_image}}, "java": {"21": {"image": JAVA_IMAGE}}}, } ) def configuration(*, requires: list[str] | None = None, toolchain: str = "android", tool: str = "script") -> dict[str, object]: """One component running the checked-in Android helper; `requires` is the explicit profile declaration.""" toolchains = {toolchain: "2026.10.1" if toolchain == "android" else "21"} if tool == "script" else ({"android": "2026.10.1"} if tool == "android" else {"java": "21"}) build: dict[str, object] = {"tool": tool, "commands": [["./scripts/ci/android-verify.sh", "instrumented"]]} if tool == "script" else {"tool": tool, "arguments": ["verify"]} component: dict[str, object] = {"toolchains": toolchains, "build": build} if requires is not None: component["requires"] = requires return {"schema": 1, "components": {"android-verify": component}} def planned(runtimes: RuntimeCatalog, **options: Any) -> Plan: """Plan the configuration for a pull request against the given catalog.""" return plan_for(parse_project_configuration(configuration(**options)), event(), runtimes) class DeclarationTest(unittest.TestCase): """What may declare the profile, and what existing choices stay.""" def test_the_script_tool_gains_android_and_the_emulator_profile_and_keeps_every_earlier_choice(self) -> None: """Additive only: the earlier toolchain choices and profiles are all still there.""" script = BUILD_TOOLS["script"] self.assertTrue({"control", "java", "node", "xcode", "dotnet"} <= set(script.toolchain_choices)) self.assertIn("android", script.toolchain_choices) self.assertTrue({"browser", "docker", "image"} <= set(script.profiles)) self.assertIn(EMULATOR_PROFILE, script.profiles) def test_only_the_script_tool_offers_the_profile(self) -> None: """The android Gradle tool and Maven do not: the declaration is refused by name rather than ignored.""" for tool in ("android", "maven", "gradle"): with self.subTest(tool), self.assertRaisesRegex(ConfigurationError, "unsupported runtime profile"): parse_project_configuration(configuration(requires=[EMULATOR_PROFILE], tool=tool)) def test_the_one_container_option_is_the_kvm_device_and_the_browser_options_are_unchanged(self) -> None: """The emulator adds only the device request; the browser profile's options are exactly what they were.""" self.assertEqual(pool_container_options([EMULATOR_PROFILE]), "--device=/dev/kvm") self.assertEqual(pool_container_options(["browser"]), "--init --shm-size=256m") self.assertEqual(PROFILE_CONTAINER_OPTIONS["browser"], ("--init", "--shm-size=256m")) class QualifiedCatalogTest(unittest.TestCase): """When an operator HAS recorded an image and a pool (fake catalog), the existing pool job is selected with the one device option.""" def test_the_declared_capability_selects_the_pool_job_its_image_its_label_and_the_device_option(self) -> None: """execution `pool`, the recorded emulator image and label, the profile in `requires`, and the KVM option only for this target.""" plan = planned(catalog(), requires=[EMULATOR_PROFILE]) (target,) = plan["targets"] self.assertEqual((target["execution"], target["image"], target["runner"], target["requires"]), (POOL_EXECUTION, EMULATOR_IMAGE, POOL, [EMULATOR_PROFILE])) self.assertEqual(target["toolchain"], "android") self.assertNotIn("DOCKER_HOST", target["environment"], "no test-daemon variable: the emulator needs no Docker socket") (entry,) = build_matrix(plan, POOL_EXECUTION)["include"] self.assertEqual(entry["options"], "--device=/dev/kvm") self.assertEqual(entry["runner"], POOL) def test_a_component_that_does_not_declare_it_is_unchanged(self) -> None: """The same script on the android toolchain without `requires` keeps the standard image, the standard pool and no options.""" plan = planned(catalog()) (target,) = plan["targets"] self.assertEqual((target["execution"], target["image"], target["requires"]), ("container", ANDROID_IMAGE, [])) self.assertEqual(build_matrix(plan, POOL_EXECUTION)["include"], []) class UnsupportedCombinationTest(unittest.TestCase): """Everything outside the one supported shape fails by name; nothing is substituted or silently routed to a broader pool.""" def test_a_non_android_toolchain_is_refused(self) -> None: """The profile is for the android toolchain only.""" with self.assertRaisesRegex(RuntimeCatalogError, "android toolchain only"): planned(catalog(), requires=[EMULATOR_PROFILE], toolchain="java") def test_a_combination_with_another_profile_is_refused(self) -> None: """No image or pool is defined for a combination, so each is refused rather than reduced to one part.""" for other in ("docker", "browser", "image"): with self.subTest(other), self.assertRaisesRegex(RuntimeCatalogError, "cannot be combined"): planned(catalog(), requires=[EMULATOR_PROFILE, other]) def test_a_recorded_image_without_a_pool_label_and_a_pool_label_without_an_image_are_each_blocked(self) -> None: """Both facts are required: nothing falls back to the standard or the browser pool, or to the standard android image.""" with self.assertRaisesRegex(RuntimeCatalogError, "emulator runtime profile has no qualified runner pool"): planned(catalog(pool=None), requires=[EMULATOR_PROFILE]) with self.assertRaisesRegex(RuntimeCatalogError, "emulator runtime is unqualified"): planned(catalog(emulator_image=None), requires=[EMULATOR_PROFILE]) def test_a_catalog_that_lists_no_emulator_image_for_the_version_says_so(self) -> None: """An unlisted version is unsupported, by name.""" bare = parse_runtime_catalog({"schema": 1, "runners": {"linux-amd64": {"emulator": POOL}}, "toolchains": {"android": {"2026.10.1": {"image": ANDROID_IMAGE}}}}) with self.assertRaisesRegex(RuntimeCatalogError, "has no emulator runtime"): planned(bare, requires=[EMULATOR_PROFILE]) def test_the_emulator_image_must_be_pinned_by_digest(self) -> None: """A tag-only reference is rejected like every other catalog image.""" with self.assertRaisesRegex(RuntimeCatalogError, "emulator_image"): parse_runtime_catalog({"schema": 1, "toolchains": {"android": {"2026.10.1": {"image": ANDROID_IMAGE, "emulator_image": "registry.example/android-emulator:latest"}}}}) class ShippedCatalogTest(unittest.TestCase): """The real catalog: the capability is unqualified, and the existing Android image is not marked capable.""" def test_the_profile_is_blocked_by_name_with_the_shipped_catalog(self) -> None: """The real null emulator image blocks planning; nothing starts.""" shipped = load_runtime_catalog() self.assertIsNone(shipped.emulator_images["android"]["2026.10.1"]) with self.assertRaisesRegex(RuntimeCatalogError, "emulator runtime is unqualified"): planned(shipped, requires=[EMULATOR_PROFILE]) def test_no_pool_label_is_recorded_and_the_existing_android_image_is_unchanged(self) -> None: """No `emulator` pool is recorded, and the standard Android image still resolves for components that do not need an emulator.""" shipped = load_runtime_catalog() self.assertIsNone(shipped.runners["linux-amd64"].get("emulator")) self.assertIn("platform-ci-android@sha256:", shipped.resolve("android", "2026.10.1")) (target,) = planned(shipped)["targets"] self.assertEqual((target["requires"], target["execution"]), ([], "container")) if __name__ == "__main__": unittest.main()