179 lines
5.9 KiB
Python
179 lines
5.9 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import itertools
|
|
import socket
|
|
|
|
import pytest
|
|
|
|
from devplacepy.database import get_table, init_db
|
|
from devplacepy.services.containers import store
|
|
from devplacepy.services.containers.workspace import provision
|
|
|
|
OWNER = "provision-owner"
|
|
STATUSES = (
|
|
store.ST_CREATED,
|
|
store.ST_STARTING,
|
|
store.ST_RUNNING,
|
|
store.ST_STOPPED,
|
|
store.ST_PAUSED,
|
|
store.ST_CRASHED,
|
|
store.ST_RESTARTING,
|
|
store.ST_REMOVING,
|
|
store.ST_REMOVED,
|
|
"",
|
|
)
|
|
DESIRED = (store.DESIRED_RUNNING, store.DESIRED_STOPPED, store.DESIRED_PAUSED, "")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _provision_db():
|
|
init_db()
|
|
yield
|
|
get_table("instances").delete(workspace_owner_uid=OWNER)
|
|
|
|
|
|
def _instance(**overrides) -> dict:
|
|
row = {
|
|
"uid": "ws-phase",
|
|
"status": store.ST_RUNNING,
|
|
"desired_state": store.DESIRED_RUNNING,
|
|
"suspended_at": "",
|
|
"editor_port": 8443,
|
|
"ports_json": "[]",
|
|
"container_ip": "",
|
|
"workspace_owner_uid": OWNER,
|
|
}
|
|
row.update(overrides)
|
|
return row
|
|
|
|
|
|
@pytest.fixture
|
|
def listener():
|
|
sock = socket.socket()
|
|
sock.bind(("127.0.0.1", 0))
|
|
sock.listen(4)
|
|
try:
|
|
yield sock
|
|
finally:
|
|
sock.close()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"status, desired, ready, expected",
|
|
[
|
|
(store.ST_RUNNING, store.DESIRED_RUNNING, True, provision.PHASE_READY),
|
|
(store.ST_RUNNING, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
|
(store.ST_CREATED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
|
(store.ST_STOPPED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
|
(store.ST_CRASHED, store.DESIRED_RUNNING, False, provision.PHASE_STARTING),
|
|
(store.ST_RUNNING, store.DESIRED_STOPPED, True, provision.PHASE_STOPPING),
|
|
(store.ST_PAUSED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
|
|
(store.ST_RESTARTING, store.DESIRED_STOPPED, False, provision.PHASE_STOPPING),
|
|
(store.ST_CRASHED, store.DESIRED_STOPPED, False, provision.PHASE_CRASHED),
|
|
(store.ST_STOPPED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
|
|
(store.ST_CREATED, store.DESIRED_STOPPED, False, provision.PHASE_STOPPED),
|
|
("", "", False, provision.PHASE_STOPPED),
|
|
],
|
|
)
|
|
def test_phase_follows_desired_state_status_and_the_editor_probe(
|
|
status, desired, ready, expected
|
|
):
|
|
row = _instance(status=status, desired_state=desired)
|
|
assert provision.phase(row, ready) == expected
|
|
|
|
|
|
def test_a_suspended_workspace_is_suspended_whatever_else_it_says():
|
|
for status, desired, ready in itertools.product(STATUSES, DESIRED, (True, False)):
|
|
row = _instance(
|
|
status=status, desired_state=desired, suspended_at="2026-01-01T00:00:00"
|
|
)
|
|
assert provision.phase(row, ready) == provision.PHASE_SUSPENDED
|
|
|
|
|
|
def test_ready_needs_a_running_container_that_wants_to_run():
|
|
for status, desired in itertools.product(STATUSES, DESIRED):
|
|
row = _instance(status=status, desired_state=desired)
|
|
is_ready = provision.phase(row, True) == provision.PHASE_READY
|
|
assert is_ready == (
|
|
status == store.ST_RUNNING and desired == store.DESIRED_RUNNING
|
|
)
|
|
|
|
|
|
def test_every_phase_over_the_whole_input_domain_has_a_label():
|
|
for status, desired, ready, suspended in itertools.product(
|
|
STATUSES, DESIRED, (True, False), ("", "2026-01-01T00:00:00")
|
|
):
|
|
row = _instance(status=status, desired_state=desired, suspended_at=suspended)
|
|
assert provision.phase(row, ready) in provision.PHASE_LABELS
|
|
|
|
|
|
def test_transitional_phases_are_exactly_starting_and_stopping():
|
|
assert set(provision.TRANSITIONAL_PHASES) == {
|
|
provision.PHASE_STARTING,
|
|
provision.PHASE_STOPPING,
|
|
}
|
|
|
|
|
|
def test_editor_ready_when_the_editor_port_accepts_connections(listener):
|
|
port = listener.getsockname()[1]
|
|
row = _instance(container_ip="127.0.0.1", editor_port=port)
|
|
assert provision.editor_ready(row) is True
|
|
|
|
|
|
def test_editor_ready_is_false_when_nothing_listens():
|
|
probe = socket.socket()
|
|
probe.bind(("127.0.0.1", 0))
|
|
port = probe.getsockname()[1]
|
|
probe.close()
|
|
row = _instance(container_ip="127.0.0.1", editor_port=port)
|
|
assert provision.editor_ready(row) is False
|
|
|
|
|
|
def test_editor_ready_is_false_without_a_reachable_target(listener):
|
|
row = _instance(container_ip="", ports_json="[]")
|
|
assert provision.editor_ready(row) is False
|
|
|
|
|
|
def test_editor_ready_is_false_unless_the_container_runs(listener):
|
|
port = listener.getsockname()[1]
|
|
for status in STATUSES:
|
|
if status == store.ST_RUNNING:
|
|
continue
|
|
row = _instance(container_ip="127.0.0.1", editor_port=port, status=status)
|
|
assert provision.editor_ready(row) is False
|
|
|
|
|
|
def test_editor_ready_is_false_while_suspended(listener):
|
|
port = listener.getsockname()[1]
|
|
row = _instance(
|
|
container_ip="127.0.0.1", editor_port=port, suspended_at="2026-01-01T00:00:00"
|
|
)
|
|
assert provision.editor_ready(row) is False
|
|
|
|
|
|
def test_view_carries_the_phase_its_label_readiness_and_the_owner(listener):
|
|
port = listener.getsockname()[1]
|
|
instance = store.create_instance(
|
|
{
|
|
"project_uid": "provision-project",
|
|
"name": "ws-view",
|
|
"status": store.ST_RUNNING,
|
|
"desired_state": store.DESIRED_RUNNING,
|
|
"is_workspace": 1,
|
|
"workspace_owner_uid": OWNER,
|
|
"container_ip": "127.0.0.1",
|
|
"editor_port": port,
|
|
"ports_json": "[]",
|
|
}
|
|
)
|
|
view = provision.view(instance)
|
|
assert view["phase"] == provision.PHASE_READY
|
|
assert view["phase_label"] == "Ready"
|
|
assert view["editor_ready"] is True
|
|
assert view["owner_uid"] == OWNER
|
|
|
|
store.update_instance(instance["uid"], {"desired_state": store.DESIRED_STOPPED})
|
|
view = provision.view(store.get_instance(instance["uid"]))
|
|
assert view["phase"] == provision.PHASE_STOPPING
|
|
assert view["editor_ready"] is True
|