91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
410 lines
15 KiB
Python
410 lines
15 KiB
Python
"""Integration tests for Image builder methods across all guest OS types.
|
|
|
|
Every image builder method is tested on every guest OS that supports it.
|
|
Tests auto-skip via skip_if_unsupported() when the required runtime is
|
|
not installed on this host.
|
|
|
|
Run with:
|
|
pytest tests/test_image_builder.py -v
|
|
pytest tests/test_image_builder.py -v -k linux # only linux tests
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from cua import Image, Sandbox, skip_if_unsupported
|
|
|
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def _run(image: Image, cmd: str) -> str:
|
|
"""Ephemeral sandbox: run cmd and return stdout. Asserts success."""
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
r = await sb.shell.run(cmd, timeout=120)
|
|
assert r.success, f"Command failed (rc={r.returncode}): {r.stderr}"
|
|
return r.stdout.strip()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Linux (Docker container)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestLinuxAptInstall:
|
|
def _base(self):
|
|
return Image.linux()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._base())
|
|
|
|
async def test_single_package(self):
|
|
image = self._base().apt_install("curl")
|
|
out = await _run(image, "curl --version")
|
|
assert "curl" in out.lower()
|
|
|
|
async def test_multiple_packages(self):
|
|
image = self._base().apt_install("curl", "wget")
|
|
out = await _run(image, "curl --version && wget --version")
|
|
assert "curl" in out.lower()
|
|
|
|
async def test_chained_calls(self):
|
|
image = self._base().apt_install("curl").apt_install("wget")
|
|
out = await _run(image, "curl --version && wget --version")
|
|
assert "curl" in out.lower()
|
|
|
|
|
|
class TestLinuxPipInstall:
|
|
def _base(self):
|
|
return Image.linux().apt_install("python3-pip")
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_single_package(self):
|
|
image = self._base().pip_install("httpx")
|
|
out = await _run(image, "python3 -c 'import httpx; print(httpx.__version__)'")
|
|
assert out # any version string
|
|
|
|
async def test_multiple_packages(self):
|
|
image = self._base().pip_install("httpx", "rich")
|
|
out = await _run(image, "python3 -c 'import httpx, rich; print(\"ok\")'")
|
|
assert "ok" in out
|
|
|
|
|
|
class TestLinuxUvInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_single_package(self):
|
|
image = Image.linux().uv_install("httpx")
|
|
out = await _run(image, "uv run python -c 'import httpx; print(\"ok\")'")
|
|
assert "ok" in out
|
|
|
|
async def test_multiple_packages(self):
|
|
image = Image.linux().uv_install("httpx", "rich")
|
|
out = await _run(image, "uv run python -c 'import httpx, rich; print(\"ok\")'")
|
|
assert "ok" in out
|
|
|
|
|
|
class TestLinuxRun:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_writes_file(self):
|
|
image = Image.linux().run("echo 'hello' > /etc/hello.txt")
|
|
out = await _run(image, "cat /etc/hello.txt")
|
|
assert "hello" in out
|
|
|
|
async def test_creates_directory(self):
|
|
image = Image.linux().run("mkdir -p /opt/myapp")
|
|
out = await _run(image, "test -d /opt/myapp && echo exists")
|
|
assert "exists" in out
|
|
|
|
async def test_chained(self):
|
|
image = Image.linux().run("mkdir -p /opt/myapp").run("echo 'v1' > /opt/myapp/version.txt")
|
|
out = await _run(image, "cat /opt/myapp/version.txt")
|
|
assert "v1" in out
|
|
|
|
|
|
class TestLinuxEnv:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_single_variable(self):
|
|
image = Image.linux().env(MY_VAR="hello_integration")
|
|
out = await _run(image, "echo $MY_VAR")
|
|
assert "hello_integration" in out
|
|
|
|
async def test_multiple_variables(self):
|
|
image = Image.linux().env(VAR_A="alpha", VAR_B="beta")
|
|
out = await _run(image, "echo $VAR_A $VAR_B")
|
|
assert "alpha" in out
|
|
assert "beta" in out
|
|
|
|
async def test_variable_used_in_run(self):
|
|
image = Image.linux().env(APP_PORT="8080").run("echo port=$APP_PORT > /tmp/port.txt")
|
|
out = await _run(image, "cat /tmp/port.txt")
|
|
assert "8080" in out
|
|
|
|
|
|
class TestLinuxCopy:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_copy_file(self, tmp_path):
|
|
src = tmp_path / "hello.txt"
|
|
src.write_text("from host")
|
|
image = Image.linux().copy(str(src), "/tmp/hello.txt")
|
|
out = await _run(image, "cat /tmp/hello.txt")
|
|
assert "from host" in out
|
|
|
|
async def test_copy_preserves_content(self, tmp_path):
|
|
src = tmp_path / "data.json"
|
|
src.write_text('{"key": "value"}')
|
|
image = Image.linux().copy(str(src), "/tmp/data.json")
|
|
out = await _run(image, "cat /tmp/data.json")
|
|
assert '"key"' in out
|
|
|
|
|
|
class TestLinuxExpose:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_expose_port_reachable(self):
|
|
"""expose() should not raise — container starts with the port mapped."""
|
|
image = Image.linux().expose(9999)
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
# Just verify the sandbox is reachable; port mapping is a docker-run concern
|
|
r = await sb.shell.run("echo ok")
|
|
assert r.success
|
|
|
|
|
|
class TestLinuxFromRegistry:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_ubuntu_22(self):
|
|
# from_registry with plain images only works if the image has computer-server.
|
|
# Use the cua base image which always has it.
|
|
image = Image.from_registry("docker.io/trycua/cua-xfce:latest")
|
|
out = await _run(image, "cat /etc/os-release")
|
|
assert "ubuntu" in out.lower() or "linux" in out.lower()
|
|
|
|
async def test_debian(self):
|
|
# Same constraint — use cua base image.
|
|
image = Image.from_registry("docker.io/trycua/cua-xfce:latest")
|
|
out = await _run(image, "uname -s")
|
|
assert "linux" in out.lower()
|
|
|
|
|
|
class TestLinuxFullChain:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.linux())
|
|
|
|
async def test_full_chain(self, tmp_path):
|
|
src = tmp_path / "app.sh"
|
|
src.write_text("#!/bin/sh\necho app-$VERSION\n")
|
|
image = (
|
|
Image.linux()
|
|
.apt_install("curl")
|
|
.env(VERSION="1.0.0")
|
|
.copy(str(src), "/usr/local/bin/app.sh")
|
|
.run("chmod +x /usr/local/bin/app.sh")
|
|
.expose(8080)
|
|
)
|
|
out = await _run(image, "/usr/local/bin/app.sh")
|
|
assert "app-1.0.0" in out
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# macOS VM
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestMacOSBrewInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.macos())
|
|
|
|
async def test_single_package(self):
|
|
image = Image.macos().brew_install("jq")
|
|
out = await _run(image, "jq --version")
|
|
assert "jq" in out
|
|
|
|
async def test_multiple_packages(self):
|
|
image = Image.macos().brew_install("jq", "wget")
|
|
out = await _run(image, "jq --version && wget --version")
|
|
assert "jq" in out
|
|
|
|
|
|
class TestMacOSRun:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.macos())
|
|
|
|
async def test_run_command(self):
|
|
image = Image.macos().run("echo setup > /tmp/setup.txt")
|
|
out = await _run(image, "cat /tmp/setup.txt")
|
|
assert "setup" in out
|
|
|
|
|
|
class TestMacOSEnv:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.macos())
|
|
|
|
async def test_env_variable(self):
|
|
image = Image.macos().env(MY_ENV="macos_test")
|
|
out = await _run(image, "echo $MY_ENV")
|
|
assert "macos_test" in out
|
|
|
|
|
|
class TestMacOSCopy:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.macos())
|
|
|
|
async def test_copy_file(self, tmp_path):
|
|
src = tmp_path / "hello.txt"
|
|
src.write_text("hello from host")
|
|
image = Image.macos().copy(str(src), "/tmp/hello.txt")
|
|
out = await _run(image, "cat /tmp/hello.txt")
|
|
assert "hello from host" in out
|
|
|
|
|
|
class TestMacOSPipInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.macos())
|
|
|
|
async def test_pip_install(self):
|
|
image = Image.macos().pip_install("httpx")
|
|
out = await _run(image, "python3 -c 'import httpx; print(\"ok\")'")
|
|
assert "ok" in out
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Windows VM
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestWindowsChocoInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_single_package(self):
|
|
image = Image.windows().choco_install("curl")
|
|
out = await _run(image, "curl --version")
|
|
assert "curl" in out.lower()
|
|
|
|
async def test_multiple_packages(self):
|
|
image = Image.windows().choco_install("curl", "wget")
|
|
out = await _run(image, "curl --version")
|
|
assert "curl" in out.lower()
|
|
|
|
|
|
class TestWindowsWingetInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_single_package(self):
|
|
image = Image.windows().winget_install("Microsoft.PowerShell")
|
|
out = await _run(image, "pwsh --version")
|
|
assert "powershell" in out.lower()
|
|
|
|
|
|
class TestWindowsRun:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_run_powershell(self):
|
|
image = Image.windows().run("echo setup > C:\\setup.txt")
|
|
out = await _run(image, "type C:\\setup.txt")
|
|
assert "setup" in out
|
|
|
|
|
|
class TestWindowsEnv:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_env_variable(self):
|
|
image = Image.windows().env(MY_VAR="win_test")
|
|
out = await _run(image, "echo %MY_VAR%")
|
|
assert "win_test" in out
|
|
|
|
|
|
class TestWindowsCopy:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_copy_file(self, tmp_path):
|
|
src = tmp_path / "hello.txt"
|
|
src.write_text("hello windows")
|
|
image = Image.windows().copy(str(src), "C:\\hello.txt")
|
|
out = await _run(image, "type C:\\hello.txt")
|
|
assert "hello windows" in out
|
|
|
|
|
|
class TestWindowsExpose:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.windows())
|
|
|
|
async def test_expose_port(self):
|
|
image = Image.windows().expose(8080)
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
r = await sb.shell.run("echo exposed")
|
|
assert r.success
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Android VM
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestAndroidApkInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.android())
|
|
|
|
async def test_apk_install(self):
|
|
"""Basic APK install — downloads F-Droid APK from URL."""
|
|
image = Image.android().apk_install("https://f-droid.org/F-Droid.apk")
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
r = await sb.shell.run("pm list packages")
|
|
assert r.success
|
|
assert "org.fdroid" in r.stdout
|
|
|
|
async def test_apk_install_from_env(self):
|
|
import os
|
|
|
|
apk_path = os.environ.get("ANDROID_TEST_APK")
|
|
if not apk_path:
|
|
pytest.skip("ANDROID_TEST_APK not set")
|
|
image = Image.android().apk_install(apk_path)
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
r = await sb.shell.run("pm list packages")
|
|
assert r.success
|
|
|
|
|
|
class TestAndroidPwaInstall:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.android())
|
|
|
|
async def test_pwa_install(self):
|
|
"""PWA install from the android-example-gym-pwa-app manifest."""
|
|
import os
|
|
import tempfile
|
|
import urllib.request
|
|
|
|
manifest_url = os.environ.get(
|
|
"ANDROID_TEST_PWA_URL", "https://cuaai--todo-gym-web.modal.run/manifest.json"
|
|
)
|
|
keystore_url = "https://raw.githubusercontent.com/trycua/android-example-gym-pwa-app/main/android.keystore"
|
|
with tempfile.NamedTemporaryFile(suffix=".keystore", delete=False) as f:
|
|
urllib.request.urlretrieve(keystore_url, f.name)
|
|
keystore_path = f.name
|
|
image = Image.android().pwa_install(
|
|
manifest_url,
|
|
keystore=keystore_path,
|
|
keystore_alias="android",
|
|
keystore_password="android",
|
|
)
|
|
async with Sandbox.ephemeral(image, local=True) as sb:
|
|
r = await sb.shell.run("pm list packages")
|
|
assert r.success
|
|
|
|
|
|
class TestAndroidRun:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.android())
|
|
|
|
async def test_run_adb_shell(self):
|
|
image = Image.android().run("echo android > /data/local/tmp/test.txt")
|
|
out = await _run(image, "cat /data/local/tmp/test.txt")
|
|
assert "android" in out
|
|
|
|
|
|
class TestAndroidEnv:
|
|
def setup_method(self):
|
|
skip_if_unsupported(Image.android())
|
|
|
|
async def test_env_variable(self):
|
|
image = Image.android().env(MY_VAR="android_test")
|
|
out = await _run(image, "echo $MY_VAR")
|
|
assert "android_test" in out
|