"""Which core worlds can actually generate in this container.""" import os import subprocess from apworld_tester.core.config.run_settings import RunSettings from apworld_tester.pipeline.generation.command import ContainerCommand from apworld_tester.pipeline.generation.report import DriverReport class CompanionCheck: """Tries every core world once, so the rest can trust the pool. Several core worlds need a base ROM this container has no copy of, and drawing one fails the seed - with the failure landing on the apworld under test rather than on the companion that caused it. """ #: The whole verification's ceiling, and each world's share of it. #: Not configuration: the per-world limit is deliberately shorter #: than a generation timeout, because a companion slow to generate #: would multiply that cost across every seed that draws it. TIMEOUT = 1800 PER_WORLD_TIMEOUT = 120 def __init__(self, config, paths, image): self.config = config self.paths = paths self.image = image self.command = ContainerCommand(config, paths, image) def build(self, output_dir, per_world_timeout, spoiler): verify_in = ContainerCommand.VERIFY_DRIVER_PATH return [ *self.command.prefix, "-v", f"{self.paths.verify_driver}:{verify_in}:ro", "-v", (f"{self.paths.driver}:" f"{ContainerCommand.DRIVER_PATH}:ro"), "-v", (f"{output_dir}:" f"{ContainerCommand.OUTPUT_DIRECTORY}"), *self.command.rom_mounts, *self.command.common_client_mount, self.image, verify_in, "--output-dir", ContainerCommand.OUTPUT_DIRECTORY, "--timeout", str(per_world_timeout), "--spoiler", str(spoiler), ] @staticmethod def not_verified(detail): return {"ran": False, "verified": [], "rejected": {}, "detail": detail} def parse(self, result): data = DriverReport(result).data if data is not None: data.setdefault("ran", True) return data return self.not_verified( f"no JSON output (exit {result.returncode}): " f"{DriverReport.tail(result.stderr)}") def run(self, output_dir, timeout=None, per_world_timeout=None, spoiler=None): """The pool of usable companions, or why there is none.""" timeout = timeout or self.TIMEOUT per_world_timeout = per_world_timeout or self.PER_WORLD_TIMEOUT if spoiler is None: spoiler = RunSettings(self.config).spoiler output_dir = os.path.abspath(output_dir) os.makedirs(output_dir, exist_ok=True) try: result = subprocess.run( self.build(output_dir, per_world_timeout, spoiler), capture_output=True, text=True, timeout=timeout, ) except subprocess.TimeoutExpired: return self.not_verified("companion verification did not " f"finish within {timeout}s") return self.parse(result)