"""Browser fixture with the real API/job workers and simulated guest execution.""" from __future__ import annotations import argparse import json import shlex import threading from dataclasses import replace from pathlib import Path from types import SimpleNamespace from unittest.mock import patch import uvicorn from nest.server import create_app from tests.browser_session_server import BrowserLifecycle from uvm.config import Settings from uvm.domain import new_vm_id from uvm.errors import ConflictError, GuestExecutionError from uvm.guest_exec import ExecResult from uvm.state import StateStore from uvm.tool_store import ToolJobStore from uvm.tools import ToolService class FixtureConnection: def __init__(self): self.closed = threading.Event() def close(self): self.closed.set() class ToolBrowserLifecycle(BrowserLifecycle): def __init__(self, settings, state, *, delay=1.2): super().__init__() self.settings, self.state, self.delay = settings, state, delay self.tools = ToolJobStore(settings, state) self.installed = {} self.attempts = {} for index, vm in enumerate(self.records.values()): vm.runtime_owned = True vm.pid, vm.process_start_time, vm.boot_id = index + 100, "1", "fixture-boot" settings.vm_dir(vm.id).mkdir(parents=True, mode=0o700, exist_ok=True) with state.transaction() as document: document.vms.update(self.records) def create(self, spec): vm = replace(self.vm, id=new_vm_id(), name=spec.name, username=spec.username, cpu=spec.cpu, ram_mib=spec.ram_mib, storage_mib=spec.storage_mib, ports=spec.ports) with self.state.operation_lock(): self.settings.vm_dir(vm.id).mkdir(mode=0o700) self.records[vm.id] = vm with self.state.transaction() as document: document.vms[vm.id] = vm if spec.tools: self.tools.enqueue_locked(vm.id, spec.tools, source="create") return vm def queue_tools(self, vm_id, selections, **options): self.state.get(vm_id) with self.state.operation_lock(): return self.tools.enqueue_locked(vm_id, selections, **options) def prepare_tool_install(self, vm_id, *, start_if_stopped=False, restart=False): with self.state.operation_lock(): vm = self.records[vm_id] if vm.status == "stopped" and not (start_if_stopped or restart): raise ConflictError("start the VM first") vm.status = "running" with self.state.transaction() as document: document.vms[vm.id] = vm return vm def execute(self, vm_id, command, *, expected_identity=None, on_connect=None, **_options): self.state.get(vm_id) payload = json.loads(shlex.split(command)[-1]) connection = FixtureConnection() if on_connect: on_connect(connection) installed = self.installed.setdefault(vm_id, {"curl"}) def observation(tool): versions = {"nodejs": "24.21.0", "jdk": "21.0.12", "php": "8.5.11", "conda": "26.5.3", "micromamba": "2.9.0"} version = tool["version"] + ".15" if tool["id"] == "python" else versions.get(tool["id"], "2.0.0") return {"key": tool["key"], "id": tool["id"], "version": tool["version"], "installed": tool["key"] in installed, "detected_version": version if tool["key"] in installed else None, "error": None, "usage": tool["usage"]} if payload["action"] == "probe": result = {"ok": True, "busy": False, "boot_id": "guest-fixture", "supported": True, "tools": [observation(tool) for tool in payload["tools"]]} else: if connection.closed.wait(self.delay): raise GuestExecutionError("fixture connection closed", outcome_unknown=True) tool = payload["tools"][0] key = (vm_id, tool["key"]) self.attempts[key] = self.attempts.get(key, 0) + 1 if tool["id"] == "php" and self.attempts[key] == 1: result = {"ok": False, "error": "Simulated mirror failure", "output": "\nTry again later."} else: installed.add(tool["key"]) result = {"ok": True, "status": "installed", "tool": observation(tool), "output": f"Verified and installed {tool['key']} inside the guest."} return ExecResult(0, json.dumps(result), "", False, False) def fixture_application(base, *, delay=1.2): settings = replace(Settings(), base=base, api_token="browser-test-token", tool_workers=2) state = StateStore(settings) lifecycle = ToolBrowserLifecycle(settings, state, delay=delay) return SimpleNamespace(settings=settings, state_store=state, lifecycle=lifecycle, tools=ToolService(settings, state, lifecycle)) def main(): parser = argparse.ArgumentParser() parser.add_argument("--base", type=Path, required=True) parser.add_argument("--port", type=int, required=True) args = parser.parse_args() application = fixture_application(args.base) with patch("uvm.tools.require_root"): api = create_app(application, host="127.0.0.1") server = uvicorn.Server(uvicorn.Config(api, host="127.0.0.1", port=args.port, log_level="warning")) api.state.uvicorn_server = server server.run() if __name__ == "__main__": main()