Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions _testlib/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _request_with_retries(
*,
timeout: float = 10.0,
retries: int = 10,
backoff: float = 2.0,
backoff: float = 1.0,
Comment thread
danielvallance marked this conversation as resolved.
expected_status: int | None = 200,
verify: bool | str | None = None,
**kwargs,
Expand Down Expand Up @@ -95,7 +95,7 @@ def http_get(
*,
timeout: float = 10.0,
retries: int = 10,
backoff: float = 2.0,
backoff: float = 1.0,
expected_status: int | None = 200,
verify: bool | str | None = None,
**kwargs,
Expand All @@ -113,7 +113,7 @@ def http_post(
*,
timeout: float = 10.0,
retries: int = 10,
backoff: float = 2.0,
backoff: float = 1.0,
expected_status: int | None = 200,
verify: bool | str | None = None,
**kwargs,
Expand Down
23 changes: 23 additions & 0 deletions _testlib/unikraft.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,29 @@ def get_instance(self, target: str) -> dict[str, Any]:

return _parse_json(proc.stdout)

def wait_instance(
self, target: str, state: str, *, timeout: float | None = 150
) -> dict[str, Any]:
"""Wait until ``target`` reaches the given ``state``.

Wraps ``unikraft instances wait <target> --until state==<state>``.
Returns the parsed JSON description of the instance once it reaches
the desired state.
"""
proc = self.run(
[
"instances",
"wait",
target,
"--until",
f"state=={state}",
"--output",
"json",
],
timeout=timeout,
)
Comment on lines +162 to +173

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command does not have a --metro flag, and this setup uses a single metro

return _parse_json(proc.stdout)

def delete_instance(self, target: str) -> None:
try:
proc =self.run(["instances", "delete", target], check=False)
Expand Down
8 changes: 4 additions & 4 deletions build-environments/test_build-environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ def _cleanup_base():

# 1. Build the base image.
unikraft.build(context, base_tag)
time.sleep(3)

# 2. Run the base image; it auto-converts into a template.
unikraft.run_instance(
Expand All @@ -85,13 +84,12 @@ def _cleanup_base():
# 4. Build ROM images.
unikraft.build(context / "rom1", rom1_tag)
unikraft.build(context / "rom2", rom2_tag)
time.sleep(3)

return template_name, rom1_tag, rom2_tag


def test_build_environments_rom1(
_template_and_roms, request, unikraft, test_run_id, http
_template_and_roms, request, unikraft, test_run_id, http, wait_instance
):
"""Run an instance from the template with ROM1 and verify response."""
template_name, rom1_tag, _ = _template_and_roms
Expand All @@ -112,13 +110,14 @@ def test_build_environments_rom1(
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(instance_name, "standby")
resp = http(url)
assert resp.status_code == 200
assert "Bye, World!" in resp.text


def test_build_environments_rom2(
_template_and_roms, request, unikraft, test_run_id, http
_template_and_roms, request, unikraft, test_run_id, http, wait_instance
):
"""Run an instance from the template with ROM2 and verify response."""
template_name, _, rom2_tag = _template_and_roms
Expand All @@ -139,6 +138,7 @@ def test_build_environments_rom2(
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(instance_name, "standby")
resp = http(url)
assert resp.status_code == 200
assert "Auf Wiedersehen!" in resp.text
6 changes: 4 additions & 2 deletions caddy2.7-go1.21/test_caddy2.7-go1.21.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_caddy_serves_welcome_page(build_image, run_instance, http):
def test_caddy_serves_welcome_page(build_image, run_instance, http, wait_instance):
image = build_image("caddy2.7-go1.21", "caddy2.7-go1.21")

instance = run_instance(
Expand All @@ -24,6 +24,8 @@ def test_caddy_serves_welcome_page(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(url)
assert resp.status_code == 200
assert "Hello, world!" in resp.text
6 changes: 4 additions & 2 deletions chromium-cdp/test_chromium_cdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_chromium_version(build_image, run_instance, http):
def test_chromium_version(build_image, run_instance, http, wait_instance):
image = build_image("chromium-cdp", "chromium-cdp")

instance = run_instance(
Expand All @@ -24,6 +24,8 @@ def test_chromium_version(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(f"{url}/json/version")
assert resp.status_code == 200
assert "Browser" in resp.text
31 changes: 27 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from _testlib.socat import SocatTunnel
from _testlib.unikraft import (
UnikraftCLI,
extract_instance_name,
extract_instance_url,
)

Expand Down Expand Up @@ -138,10 +139,6 @@ def _build(example_dir: str, image_name: str) -> str:

unikraft.build(context, tag)

# TODO: drop this once the platform exposes a way to wait until a
# freshly-built image is fully available for `unikraft run`.
time.sleep(3)

return tag

return _build
Expand Down Expand Up @@ -228,6 +225,31 @@ def _cleanup() -> None:
return _run


# ---------------------------------------------------------------------------
# Instance wait fixture
# ---------------------------------------------------------------------------


WaitInstance = Callable[[str, str], dict[str, Any]]


@pytest.fixture
def wait_instance(unikraft: UnikraftCLI) -> WaitInstance:
"""Wait until a named instance reaches the given state.

Usage::

def test_foo(run_instance, wait_instance):
instance = run_instance(image, ...)
wait_instance(extract_instance_name(instance), "running")
"""

def _wait(target: str, state: str) -> dict[str, Any]:
return unikraft.wait_instance(target, state)

return _wait


# ---------------------------------------------------------------------------
# HTTP helper
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -284,4 +306,5 @@ def _create(host: str, port: int, local_port: int) -> SocatTunnel:
"run_instance",
"socat_tunnel",
"unikraft",
"wait_instance",
]
60 changes: 23 additions & 37 deletions debian-ssh/test_debian-ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@
from __future__ import annotations

import subprocess
import time
from pathlib import Path

import pytest

from _testlib.unikraft import extract_instance_fqdn
from _testlib.unikraft import extract_instance_fqdn, extract_instance_name

SSH_PORT = 2222

Expand All @@ -36,7 +35,7 @@ def _generate_test_keypair(tmp_path: Path) -> tuple[Path, str]:
return key_path, pub_key


def test_debian_ssh(build_image, run_instance, socat_tunnel, tmp_path):
def test_debian_ssh(build_image, run_instance, socat_tunnel, wait_instance, tmp_path):
"""Build, deploy, and SSH into a Debian instance."""
private_key, public_key = _generate_test_keypair(tmp_path)

Expand All @@ -58,37 +57,24 @@ def test_debian_ssh(build_image, run_instance, socat_tunnel, tmp_path):
# Set up a socat TLS tunnel to the SSH port.
tunnel = socat_tunnel(host, SSH_PORT, SSH_PORT)

# Retry SSH connection with back-off while the instance boots.
last_err = None
for _ in range(10):
try:
result = subprocess.run(
[
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=10",
"-i", str(private_key),
"-p", str(tunnel.local_port),
"-l", "root",
"127.0.0.1",
"echo hello-from-pytest",
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
assert "hello-from-pytest" in result.stdout
return
last_err = RuntimeError(
f"ssh exited {result.returncode}: {result.stderr}"
)
except (subprocess.TimeoutExpired, OSError) as exc:
last_err = exc

time.sleep(5)

raise AssertionError(
f"could not connect via SSH after retries: {last_err}"
) from last_err
wait_instance(extract_instance_name(instance), "running")
result = subprocess.run(
[
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=10",
"-i", str(private_key),
"-p", str(tunnel.local_port),
"-l", "root",
"127.0.0.1",
"echo hello-from-pytest",
],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
f"ssh failed (exit={result.returncode})\nstdout: {result.stdout}\nstderr: {result.stderr}"
)
assert "hello-from-pytest" in result.stdout
6 changes: 4 additions & 2 deletions dragonflydb/test_dragonflydb.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_dragonflydb(build_image, run_instance, http):
def test_dragonflydb(build_image, run_instance, http, wait_instance):
"""Build, deploy, and exercise a DragonflyDB instance."""
image = build_image("dragonflydb", "dragonflydb")

Expand All @@ -31,6 +31,8 @@ def test_dragonflydb(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

# http() already retries with back-off, so no manual sleep needed.
# ------------------------------------------------------------------
# 1. HTTP status page — matches existing CI workflow (curl check).
Expand Down
6 changes: 4 additions & 2 deletions duckdb-go1.21/test_duckdb-go1.21.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_duckdb_go_serves_query_result(build_image, run_instance, http):
def test_duckdb_go_serves_query_result(build_image, run_instance, http, wait_instance):
image = build_image("duckdb-go1.21", "duckdb-go1.21")

instance = run_instance(
Expand All @@ -24,6 +24,8 @@ def test_duckdb_go_serves_query_result(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(url)
assert resp.status_code == 200
assert "42" in resp.text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_httpserver_serves_hello(build_image, run_instance, http):
def test_httpserver_serves_hello(build_image, run_instance, http, wait_instance):
image = build_image("feature-change-instance-cmd", "feature-change-instance-cmd")

instance = run_instance(
Expand All @@ -24,6 +24,8 @@ def test_httpserver_serves_hello(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(url)
assert resp.status_code == 200
assert "Hello, World!" in resp.text
6 changes: 4 additions & 2 deletions github-webhook-node/test_github-webhook-node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_github_webhook_health(build_image, run_instance, http):
def test_github_webhook_health(build_image, run_instance, http, wait_instance):
image = build_image("github-webhook-node", "github-webhook-node")

instance = run_instance(
Expand All @@ -27,6 +27,8 @@ def test_github_webhook_health(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(f"{url}/health")
assert resp.status_code == 200

Expand Down
6 changes: 4 additions & 2 deletions grafana/test_grafana.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

from __future__ import annotations

from _testlib.unikraft import extract_instance_url
from _testlib.unikraft import extract_instance_name, extract_instance_url


def test_grafana(build_image, run_instance, http):
def test_grafana(build_image, run_instance, http, wait_instance):
"""Build, deploy, and verify Grafana serves its login page."""
image = build_image("grafana", "grafana")

Expand All @@ -26,6 +26,8 @@ def test_grafana(build_image, run_instance, http):
url = extract_instance_url(instance)
assert url, f"could not determine instance URL from: {instance!r}"

wait_instance(extract_instance_name(instance), "running")

resp = http(url)
assert resp.status_code == 200
body = resp.text.lower()
Expand Down
Loading
Loading