Archived
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
149 lines
9.1 KiB
Python
149 lines
9.1 KiB
Python
"""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()
|