feat: made an integration module for integration tests
Minor fixes to the unit tests
This commit is contained in:
56
AGENTS.md
Normal file
56
AGENTS.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Wrenn Python SDK — a client library for the Wrenn microVM platform. e2b drop-in replacement.
|
||||||
|
Package name: `wrenn`. Python 3.13+, managed with [uv](https://docs.astral.sh/uv/).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync # install deps
|
||||||
|
make lint # ruff check + format check (no auto-fix)
|
||||||
|
make test # unit tests only (tests/test_client.py)
|
||||||
|
make test-integration # all tests including integration (needs live server)
|
||||||
|
make generate # regenerate models from OpenAPI spec (fetches from remote)
|
||||||
|
make check # lint + unit test
|
||||||
|
```
|
||||||
|
|
||||||
|
- `make test` only runs `tests/test_client.py`, not all unit tests. To run a specific test file: `uv run pytest tests/test_capsule_features.py -v`
|
||||||
|
- No typecheck step in Makefile or CI. `mypy` is a dev dependency but not wired up — do not assume it runs.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `src/wrenn/` — the library package
|
||||||
|
- `capsule.py` / `async_capsule.py` — high-level `Capsule` / `AsyncCapsule` (main user-facing classes)
|
||||||
|
- `client.py` — low-level `WrennClient` / `AsyncWrennClient`
|
||||||
|
- `commands.py` — command execution and streaming
|
||||||
|
- `files.py` — filesystem operations
|
||||||
|
- `pty.py` — interactive terminal (PTY) over WebSocket
|
||||||
|
- `exceptions.py` — typed error hierarchy (`WrennError` base)
|
||||||
|
- `models/_generated.py` — **auto-generated** from OpenAPI spec via `datamodel-codegen` (never edit directly; run `make generate`)
|
||||||
|
- `sandbox.py` — deprecated `Sandbox` alias for `Capsule`
|
||||||
|
- `code_interpreter/` — specialized capsule for stateful Jupyter kernel execution
|
||||||
|
- `tests/` — unit tests use `respx` to mock `httpx`; integration tests are in `tests/integration/`
|
||||||
|
- `api/openapi.yaml` — downloaded OpenAPI spec used for code generation
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- Generated code lives in `src/wrenn/models/_generated.py`. Never edit it. Run `make generate` to update.
|
||||||
|
- `Sandbox` is a deprecated alias for `Capsule`. New code should use `Capsule` / `AsyncCapsule`.
|
||||||
|
- Dual sync/async API: every major class has an `Async` counterpart.
|
||||||
|
- Uses `httpx` for HTTP, `httpx-ws` for WebSockets, `pydantic` for models.
|
||||||
|
- `__init__.py` uses `__getattr__` for lazy deprecated aliases (`Sandbox`, `WrennHostHasSandboxesError`).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Unit tests mock HTTP via `respx` (httpx mocking library).
|
||||||
|
- Integration tests require env vars: `WRENN_API_KEY` (or `WRENN_TOKEN`), optionally `WRENN_BASE_URL`.
|
||||||
|
- Integration test fixtures in `tests/integration/conftest.py` create real capsules and clean them up.
|
||||||
|
- `pytest` marker: `@pytest.mark.integration` for tests needing a live server.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Woodpecker CI (`.woodpecker/check.yml`) runs on push to `main` and `dev`:
|
||||||
|
1. `make lint`
|
||||||
|
2. `make test` (unit tests only — integration tests are not in CI)
|
||||||
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
104
tests/integration/conftest.py
Normal file
104
tests/integration/conftest.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections.abc import AsyncGenerator, Generator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from wrenn.async_capsule import AsyncCapsule
|
||||||
|
from wrenn.capsule import Capsule
|
||||||
|
from wrenn.client import AsyncWrennClient, WrennClient
|
||||||
|
|
||||||
|
WRENN_API_KEY = os.environ.get("WRENN_API_KEY")
|
||||||
|
WRENN_BASE_URL = os.environ.get("WRENN_BASE_URL", "http://localhost:8080")
|
||||||
|
|
||||||
|
_env_loaded = False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_env() -> None:
|
||||||
|
global _env_loaded
|
||||||
|
if _env_loaded:
|
||||||
|
return
|
||||||
|
_env_loaded = True
|
||||||
|
env_file = Path(__file__).resolve().parent.parent / ".env"
|
||||||
|
if not env_file.exists():
|
||||||
|
return
|
||||||
|
for line in env_file.read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key, value = key.strip(), value.strip().strip("\"'")
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _load_env():
|
||||||
|
_ensure_env()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_auth() -> bool:
|
||||||
|
return bool(WRENN_API_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
requires_auth = pytest.mark.skipif(
|
||||||
|
not _has_auth(),
|
||||||
|
reason="Set WRENN_API_KEY to run integration tests",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> Generator[WrennClient, None, None]:
|
||||||
|
with WrennClient(
|
||||||
|
api_key=WRENN_API_KEY,
|
||||||
|
base_url=WRENN_BASE_URL,
|
||||||
|
) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def async_client() -> AsyncGenerator[AsyncWrennClient, None]:
|
||||||
|
async with AsyncWrennClient(api_key=WRENN_API_KEY, base_url=WRENN_BASE_URL) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def async_minimal_capsule() -> AsyncGenerator[AsyncCapsule, None]:
|
||||||
|
"""Provides a ready-to-use minimal capsule and cleans it up afterward."""
|
||||||
|
async with await AsyncCapsule.create(
|
||||||
|
template="minimal",
|
||||||
|
timeout=120,
|
||||||
|
wait=True,
|
||||||
|
api_key=WRENN_API_KEY,
|
||||||
|
base_url=WRENN_BASE_URL,
|
||||||
|
) as cap:
|
||||||
|
yield cap
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def async_python_capsule() -> AsyncGenerator[AsyncCapsule, None]:
|
||||||
|
"""Provides a ready-to-use Python interpreter capsule."""
|
||||||
|
async with await AsyncCapsule.create(
|
||||||
|
template="python-interpreter-v0-beta",
|
||||||
|
timeout=120,
|
||||||
|
wait=True,
|
||||||
|
api_key=WRENN_API_KEY,
|
||||||
|
base_url=WRENN_BASE_URL,
|
||||||
|
) as cap:
|
||||||
|
yield cap
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def minimal_capsule() -> Generator[Capsule, None, None]:
|
||||||
|
"""Provides a ready-to-use minimal capsule and cleans it up afterward."""
|
||||||
|
with Capsule(
|
||||||
|
template="minimal",
|
||||||
|
timeout=120,
|
||||||
|
wait=True,
|
||||||
|
api_key=WRENN_API_KEY,
|
||||||
|
base_url=WRENN_BASE_URL,
|
||||||
|
) as cap:
|
||||||
|
yield cap
|
||||||
91
tests/integration/test_commands.py
Normal file
91
tests/integration/test_commands.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wrenn import Capsule, CommandResult
|
||||||
|
from wrenn.commands import CommandHandle, ProcessInfo
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommands:
|
||||||
|
"""Shared capsule for command execution tests."""
|
||||||
|
|
||||||
|
capsule: Capsule
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
cls.capsule = Capsule(wait=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_class(cls):
|
||||||
|
try:
|
||||||
|
cls.capsule.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_run_foreground(self):
|
||||||
|
result = self.capsule.commands.run("echo hello")
|
||||||
|
assert isinstance(result, CommandResult)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "hello" in result.stdout
|
||||||
|
|
||||||
|
def test_run_stderr(self):
|
||||||
|
result = self.capsule.commands.run("echo error >&2")
|
||||||
|
assert "error" in result.stderr
|
||||||
|
|
||||||
|
def test_run_exit_code(self):
|
||||||
|
result = self.capsule.commands.run("exit 42")
|
||||||
|
assert result.exit_code == 42
|
||||||
|
|
||||||
|
def test_run_with_envs(self):
|
||||||
|
result = self.capsule.commands.run("export MY_VAR=test_value && echo $MY_VAR")
|
||||||
|
assert "test_value" in result.stdout
|
||||||
|
|
||||||
|
def test_run_with_cwd(self):
|
||||||
|
result = self.capsule.commands.run("cd /tmp && pwd")
|
||||||
|
assert result.stdout.strip() == "/tmp"
|
||||||
|
|
||||||
|
def test_run_multiline_output(self):
|
||||||
|
result = self.capsule.commands.run("echo -e 'line1\\nline2\\nline3'")
|
||||||
|
assert result.exit_code == 0
|
||||||
|
lines = result.stdout.strip().splitlines()
|
||||||
|
assert len(lines) == 3
|
||||||
|
|
||||||
|
def test_run_background(self):
|
||||||
|
handle = self.capsule.commands.run("sleep 30", background=True, tag="bg-test")
|
||||||
|
assert isinstance(handle, CommandHandle)
|
||||||
|
assert handle.pid > 0
|
||||||
|
assert handle.tag == "bg-test"
|
||||||
|
assert handle.capsule_id == self.capsule.capsule_id
|
||||||
|
|
||||||
|
self.capsule.commands.kill(handle.pid)
|
||||||
|
|
||||||
|
def test_list_processes(self):
|
||||||
|
handle = self.capsule.commands.run("sleep 30", background=True, tag="list-test")
|
||||||
|
try:
|
||||||
|
time.sleep(0.5)
|
||||||
|
processes = self.capsule.commands.list()
|
||||||
|
assert isinstance(processes, list)
|
||||||
|
pids = [p.pid for p in processes]
|
||||||
|
assert handle.pid in pids
|
||||||
|
|
||||||
|
proc = next(p for p in processes if p.pid == handle.pid)
|
||||||
|
assert isinstance(proc, ProcessInfo)
|
||||||
|
finally:
|
||||||
|
self.capsule.commands.kill(handle.pid)
|
||||||
|
|
||||||
|
def test_kill_process(self):
|
||||||
|
handle = self.capsule.commands.run("sleep 30", background=True)
|
||||||
|
self.capsule.commands.kill(handle.pid)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
processes = self.capsule.commands.list()
|
||||||
|
pids = [p.pid for p in processes]
|
||||||
|
assert handle.pid not in pids
|
||||||
|
|
||||||
|
def test_run_duration_ms(self):
|
||||||
|
result = self.capsule.commands.run("sleep 1")
|
||||||
|
assert result.duration_ms is None or result.duration_ms >= 900
|
||||||
95
tests/integration/test_files.py
Normal file
95
tests/integration/test_files.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wrenn import Capsule
|
||||||
|
from wrenn.models import FileEntry
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
class TestFiles:
|
||||||
|
"""Shared capsule for filesystem tests."""
|
||||||
|
|
||||||
|
capsule: Capsule
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
cls.capsule = Capsule(wait=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_class(cls):
|
||||||
|
try:
|
||||||
|
cls.capsule.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_write_and_read(self):
|
||||||
|
self.capsule.files.write("/tmp/test.txt", "hello world")
|
||||||
|
content = self.capsule.files.read("/tmp/test.txt")
|
||||||
|
assert content == "hello world"
|
||||||
|
|
||||||
|
def test_write_and_read_bytes(self):
|
||||||
|
data = b"\x00\x01\x02\xff"
|
||||||
|
self.capsule.files.write("/tmp/test.bin", data)
|
||||||
|
result = self.capsule.files.read_bytes("/tmp/test.bin")
|
||||||
|
assert result == data
|
||||||
|
|
||||||
|
def test_list_directory(self):
|
||||||
|
self.capsule.files.write("/tmp/listdir/a.txt", "a")
|
||||||
|
self.capsule.files.write("/tmp/listdir/b.txt", "b")
|
||||||
|
entries = self.capsule.files.list("/tmp/listdir")
|
||||||
|
assert isinstance(entries, list)
|
||||||
|
names = [e.name for e in entries]
|
||||||
|
assert "a.txt" in names
|
||||||
|
assert "b.txt" in names
|
||||||
|
|
||||||
|
def test_exists(self):
|
||||||
|
self.capsule.files.write("/tmp/exists_test.txt", "x")
|
||||||
|
assert self.capsule.files.exists("/tmp/exists_test.txt")
|
||||||
|
assert not self.capsule.files.exists("/tmp/does_not_exist_xyz.txt")
|
||||||
|
|
||||||
|
def test_make_dir(self):
|
||||||
|
entry = self.capsule.files.make_dir("/tmp/newdir")
|
||||||
|
assert isinstance(entry, FileEntry)
|
||||||
|
assert self.capsule.files.exists("/tmp/newdir")
|
||||||
|
|
||||||
|
def test_make_dir_idempotent(self):
|
||||||
|
self.capsule.files.make_dir("/tmp/idempotent_dir")
|
||||||
|
entry = self.capsule.files.make_dir("/tmp/idempotent_dir")
|
||||||
|
assert isinstance(entry, FileEntry)
|
||||||
|
|
||||||
|
def test_remove_file(self):
|
||||||
|
self.capsule.files.write("/tmp/to_remove.txt", "delete me")
|
||||||
|
assert self.capsule.files.exists("/tmp/to_remove.txt")
|
||||||
|
self.capsule.files.remove("/tmp/to_remove.txt")
|
||||||
|
assert not self.capsule.files.exists("/tmp/to_remove.txt")
|
||||||
|
|
||||||
|
def test_remove_directory(self):
|
||||||
|
self.capsule.files.make_dir("/tmp/dir_to_remove")
|
||||||
|
self.capsule.files.write("/tmp/dir_to_remove/child.txt", "data")
|
||||||
|
self.capsule.files.remove("/tmp/dir_to_remove")
|
||||||
|
assert not self.capsule.files.exists("/tmp/dir_to_remove")
|
||||||
|
|
||||||
|
def test_write_creates_parent_dirs(self):
|
||||||
|
self.capsule.files.write("/tmp/deep/nested/dir/file.txt", "nested")
|
||||||
|
content = self.capsule.files.read("/tmp/deep/nested/dir/file.txt")
|
||||||
|
assert content == "nested"
|
||||||
|
|
||||||
|
def test_list_with_depth(self):
|
||||||
|
self.capsule.files.write("/tmp/depth_test/a/b.txt", "deep")
|
||||||
|
entries_shallow = self.capsule.files.list("/tmp/depth_test", depth=1)
|
||||||
|
entries_deep = self.capsule.files.list("/tmp/depth_test", depth=2)
|
||||||
|
assert len(entries_deep) >= len(entries_shallow)
|
||||||
|
|
||||||
|
def test_overwrite_file(self):
|
||||||
|
self.capsule.files.write("/tmp/overwrite.txt", "original")
|
||||||
|
self.capsule.files.write("/tmp/overwrite.txt", "updated")
|
||||||
|
content = self.capsule.files.read("/tmp/overwrite.txt")
|
||||||
|
assert content == "updated"
|
||||||
|
|
||||||
|
def test_upload_and_download_stream(self):
|
||||||
|
chunks = [b"chunk1", b"chunk2", b"chunk3"]
|
||||||
|
self.capsule.files.upload_stream("/tmp/streamed.bin", iter(chunks))
|
||||||
|
downloaded = b"".join(self.capsule.files.download_stream("/tmp/streamed.bin"))
|
||||||
|
assert downloaded == b"chunk1chunk2chunk3"
|
||||||
94
tests/integration/test_git.py
Normal file
94
tests/integration/test_git.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wrenn import Capsule
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
class TestGit:
|
||||||
|
"""Shared capsule for git operation tests.
|
||||||
|
|
||||||
|
Initializes a repo at /root (default cwd) since the exec API
|
||||||
|
does not support the cwd parameter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
capsule: Capsule
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls):
|
||||||
|
cls.capsule = Capsule(wait=True)
|
||||||
|
cls.capsule.git.init(".", initial_branch="main")
|
||||||
|
cls.capsule.git.configure_user("Test User", "test@example.com")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def teardown_class(cls):
|
||||||
|
try:
|
||||||
|
cls.capsule.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_init_created_repo(self):
|
||||||
|
assert self.capsule.files.exists("/root/.git")
|
||||||
|
|
||||||
|
def test_status_clean(self):
|
||||||
|
status = self.capsule.git.status()
|
||||||
|
assert status.branch == "main"
|
||||||
|
|
||||||
|
def test_add_and_commit(self):
|
||||||
|
self.capsule.files.write("/root/hello.txt", "hello git")
|
||||||
|
self.capsule.git.add(all=True)
|
||||||
|
result = self.capsule.git.commit("initial commit")
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
def test_status_after_commit(self):
|
||||||
|
status = self.capsule.git.status()
|
||||||
|
assert status.is_clean
|
||||||
|
|
||||||
|
def test_status_with_changes(self):
|
||||||
|
self.capsule.files.write("/root/dirty.txt", "uncommitted")
|
||||||
|
try:
|
||||||
|
status = self.capsule.git.status()
|
||||||
|
assert not status.is_clean
|
||||||
|
paths = [f.path for f in status.files]
|
||||||
|
assert "dirty.txt" in paths
|
||||||
|
finally:
|
||||||
|
self.capsule.files.remove("/root/dirty.txt")
|
||||||
|
|
||||||
|
def test_branches(self):
|
||||||
|
branches = self.capsule.git.branches()
|
||||||
|
assert len(branches) >= 1
|
||||||
|
names = [b.name for b in branches]
|
||||||
|
assert "main" in names
|
||||||
|
current = [b for b in branches if b.is_current]
|
||||||
|
assert len(current) == 1
|
||||||
|
|
||||||
|
def test_create_and_checkout_branch(self):
|
||||||
|
self.capsule.git.create_branch("feature-1")
|
||||||
|
branches = self.capsule.git.branches()
|
||||||
|
names = [b.name for b in branches]
|
||||||
|
assert "feature-1" in names
|
||||||
|
|
||||||
|
current = [b for b in branches if b.is_current]
|
||||||
|
assert current[0].name == "feature-1"
|
||||||
|
|
||||||
|
self.capsule.git.checkout_branch("main")
|
||||||
|
|
||||||
|
def test_delete_branch(self):
|
||||||
|
self.capsule.git.create_branch("to-delete")
|
||||||
|
self.capsule.git.checkout_branch("main")
|
||||||
|
self.capsule.git.delete_branch("to-delete")
|
||||||
|
|
||||||
|
branches = self.capsule.git.branches()
|
||||||
|
names = [b.name for b in branches]
|
||||||
|
assert "to-delete" not in names
|
||||||
|
|
||||||
|
def test_set_and_get_config(self):
|
||||||
|
self.capsule.git.set_config("test.key", "test-value")
|
||||||
|
value = self.capsule.git.get_config("test.key")
|
||||||
|
assert value == "test-value"
|
||||||
|
|
||||||
|
def test_get_config_missing_returns_none(self):
|
||||||
|
value = self.capsule.git.get_config("nonexistent.key")
|
||||||
|
assert value is None
|
||||||
119
tests/integration/test_lifecycle.py
Normal file
119
tests/integration/test_lifecycle.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wrenn import Capsule
|
||||||
|
from wrenn.models import Capsule as CapsuleModel, Status
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
class TestCapsuleLifecycle:
|
||||||
|
"""Each test manages its own capsule to test create/destroy paths."""
|
||||||
|
|
||||||
|
def test_create_and_destroy(self):
|
||||||
|
capsule = Capsule()
|
||||||
|
capsule_id = capsule.capsule_id
|
||||||
|
try:
|
||||||
|
assert capsule_id
|
||||||
|
assert capsule.info is not None
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
info = Capsule.get_info(capsule_id)
|
||||||
|
assert info.status in (Status.stopped, Status.missing)
|
||||||
|
|
||||||
|
def test_create_with_wait(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
assert capsule.info is not None
|
||||||
|
assert capsule.info.status == Status.running
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_context_manager_destroys(self):
|
||||||
|
with Capsule(wait=True) as capsule:
|
||||||
|
capsule_id = capsule.capsule_id
|
||||||
|
assert capsule.is_running()
|
||||||
|
|
||||||
|
info = Capsule.get_info(capsule_id)
|
||||||
|
assert info.status in (Status.stopped, Status.missing)
|
||||||
|
|
||||||
|
def test_get_info(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
info = capsule.get_info()
|
||||||
|
assert isinstance(info, CapsuleModel)
|
||||||
|
assert info.id == capsule.capsule_id
|
||||||
|
assert info.status == Status.running
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_pause_and_resume(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
paused = capsule.pause()
|
||||||
|
assert paused.status == Status.paused
|
||||||
|
assert not capsule.is_running()
|
||||||
|
|
||||||
|
resumed = capsule.resume()
|
||||||
|
assert resumed.status == Status.running
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_static_destroy(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
capsule_id = capsule.capsule_id
|
||||||
|
try:
|
||||||
|
Capsule.destroy(capsule_id)
|
||||||
|
except Exception:
|
||||||
|
capsule.destroy()
|
||||||
|
raise
|
||||||
|
|
||||||
|
info = Capsule.get_info(capsule_id)
|
||||||
|
assert info.status in (Status.stopped, Status.missing)
|
||||||
|
|
||||||
|
def test_connect_to_existing(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
connected = Capsule.connect(capsule.capsule_id)
|
||||||
|
assert connected.capsule_id == capsule.capsule_id
|
||||||
|
assert connected.info is not None
|
||||||
|
assert connected.info.status == Status.running
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_connect_resumes_paused(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
capsule.pause()
|
||||||
|
connected = Capsule.connect(capsule.capsule_id)
|
||||||
|
assert connected.info is not None
|
||||||
|
assert connected.info.status == Status.running
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_list_capsules(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
capsules = Capsule.list()
|
||||||
|
assert isinstance(capsules, list)
|
||||||
|
ids = [c.id for c in capsules]
|
||||||
|
assert capsule.capsule_id in ids
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_wait_ready(self):
|
||||||
|
capsule = Capsule()
|
||||||
|
try:
|
||||||
|
capsule.wait_ready(timeout=60)
|
||||||
|
assert capsule.is_running()
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
|
|
||||||
|
def test_ping(self):
|
||||||
|
capsule = Capsule(wait=True)
|
||||||
|
try:
|
||||||
|
capsule.ping()
|
||||||
|
finally:
|
||||||
|
capsule.destroy()
|
||||||
Reference in New Issue
Block a user