feat(projects): automate project infrastructure provisioning
This commit is contained in:
@@ -43,6 +43,15 @@ class _FakeConnection:
|
||||
return self._cursor
|
||||
|
||||
|
||||
class _SequenceCursor(_FakeCursor):
|
||||
def __init__(self, rows: list[dict]) -> None:
|
||||
super().__init__()
|
||||
self._fetch_rows = iter(rows)
|
||||
|
||||
def fetchone(self):
|
||||
return next(self._fetch_rows)
|
||||
|
||||
|
||||
def _admin_connection(cursor: _FakeCursor):
|
||||
@contextmanager
|
||||
def connection():
|
||||
@@ -51,14 +60,22 @@ def _admin_connection(cursor: _FakeCursor):
|
||||
return connection
|
||||
|
||||
|
||||
def _project_connection(cursor: _FakeCursor):
|
||||
@contextmanager
|
||||
def connection(_name):
|
||||
yield _FakeConnection(cursor)
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"postgres",
|
||||
"project",
|
||||
"system_hub",
|
||||
"SYSTEM_HUB",
|
||||
"tjwater_v2_template",
|
||||
"tjwater_v2_schema_template",
|
||||
"another_template",
|
||||
],
|
||||
)
|
||||
@@ -109,12 +126,12 @@ def test_create_project_allows_the_protected_template_as_source(monkeypatch) ->
|
||||
|
||||
projects.create_project("project_a")
|
||||
|
||||
assert closed == ["tjwater_v2_template", "project_a"]
|
||||
assert any(call[1] == ("tjwater_v2_template",) for call in cursor.calls)
|
||||
assert closed == ["tjwater_v2_schema_template", "project_a"]
|
||||
assert any(call[1] == ("tjwater_v2_schema_template",) for call in cursor.calls)
|
||||
assert any(
|
||||
isinstance(call[0], str)
|
||||
and call[0].startswith("select pg_terminate_backend")
|
||||
and call[1] == ("tjwater_v2_template",)
|
||||
and call[1] == ("tjwater_v2_schema_template",)
|
||||
for call in cursor.calls
|
||||
)
|
||||
assert any("create database" in str(call[0]).lower() for call in cursor.calls)
|
||||
@@ -127,8 +144,7 @@ def test_list_project_excludes_metadata_database(monkeypatch) -> None:
|
||||
assert projects.list_project() == ["project_a"]
|
||||
excluded = cursor.calls[0][1][0]
|
||||
assert "system_hub" in excluded
|
||||
assert "project" in excluded
|
||||
assert "tjwater_v2_template" in excluded
|
||||
assert "tjwater_v2_schema_template" in excluded
|
||||
|
||||
|
||||
def test_delete_project_uses_routed_physical_database_name(monkeypatch) -> None:
|
||||
@@ -177,6 +193,63 @@ def test_temporary_database_capacity_rejects_creation_at_limit(
|
||||
assert any("pg_advisory_unlock" in str(statement) for statement, _ in cursor.calls)
|
||||
|
||||
|
||||
def test_project_model_template_requires_ready_active_subscription(monkeypatch) -> None:
|
||||
cursor = _SequenceCursor(
|
||||
[
|
||||
{
|
||||
"subscriptions": 1,
|
||||
"enabled_subscriptions": 1,
|
||||
"active_workers": 1,
|
||||
},
|
||||
{"relations": 32, "pending_relations": 0},
|
||||
]
|
||||
)
|
||||
closed: list[str] = []
|
||||
monkeypatch.setattr(projects, "project_connection", _project_connection(cursor))
|
||||
monkeypatch.setattr(projects, "close_project_pool", closed.append)
|
||||
|
||||
projects._ensure_project_model_template_ready("project_a_template")
|
||||
|
||||
assert closed == ["project_a_template"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "relations", "message"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"subscriptions": 1,
|
||||
"enabled_subscriptions": 1,
|
||||
"active_workers": 0,
|
||||
},
|
||||
{"relations": 32, "pending_relations": 0},
|
||||
"subscription is not active",
|
||||
),
|
||||
(
|
||||
{
|
||||
"subscriptions": 1,
|
||||
"enabled_subscriptions": 1,
|
||||
"active_workers": 1,
|
||||
},
|
||||
{"relations": 32, "pending_relations": 1},
|
||||
"still synchronizing",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_project_model_template_rejects_unready_subscription(
|
||||
monkeypatch, status, relations, message
|
||||
) -> None:
|
||||
cursor = _SequenceCursor([status, relations])
|
||||
closed: list[str] = []
|
||||
monkeypatch.setattr(projects, "project_connection", _project_connection(cursor))
|
||||
monkeypatch.setattr(projects, "close_project_pool", closed.append)
|
||||
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
projects._ensure_project_model_template_ready("project_a_template")
|
||||
|
||||
assert closed == ["project_a_template"]
|
||||
|
||||
|
||||
def test_temporary_project_database_cleans_up_after_failure(monkeypatch) -> None:
|
||||
calls: list[tuple[str, ...]] = []
|
||||
monkeypatch.setattr(
|
||||
@@ -187,7 +260,9 @@ def test_temporary_project_database_cleans_up_after_failure(monkeypatch) -> None
|
||||
monkeypatch.setattr(
|
||||
projects,
|
||||
"copy_project",
|
||||
lambda source, target: calls.append(("copy", source, target)),
|
||||
lambda source, target, **kwargs: calls.append(
|
||||
("copy", source, target, str(kwargs.get("allow_template_source")))
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(projects, "have_project", lambda name: True)
|
||||
monkeypatch.setattr(
|
||||
@@ -195,26 +270,13 @@ def test_temporary_project_database_cleans_up_after_failure(monkeypatch) -> None
|
||||
"delete_project",
|
||||
lambda name: calls.append(("delete", name)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.native.wndb.core.model_replace.replace_project_model",
|
||||
lambda target, source, *, copy_source_scada: calls.append(
|
||||
("clone", target, source, str(copy_source_scada))
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.native.wndb.core.database.refresh_materialized_views_after_commit",
|
||||
lambda name: calls.append(("refresh", name)),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="analysis failed"):
|
||||
with projects.temporary_project_database("project_a", "age") as name:
|
||||
assert name == "isolated_run"
|
||||
raise RuntimeError("analysis failed")
|
||||
|
||||
assert calls == [
|
||||
("copy", "tjwater_v2_template", "isolated_run"),
|
||||
("clone", "isolated_run", "project_a", "True"),
|
||||
("refresh", "isolated_run"),
|
||||
("copy", "project_a_template", "isolated_run", "True"),
|
||||
("delete", "isolated_run"),
|
||||
]
|
||||
|
||||
@@ -229,7 +291,9 @@ def test_temporary_template_database_does_not_clone_a_project(monkeypatch) -> No
|
||||
monkeypatch.setattr(
|
||||
projects,
|
||||
"copy_project",
|
||||
lambda source, target: calls.append(("copy", source, target)),
|
||||
lambda source, target, **kwargs: calls.append(
|
||||
("copy", source, target, str(kwargs.get("allow_template_source")))
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(projects, "have_project", lambda name: True)
|
||||
monkeypatch.setattr(
|
||||
@@ -242,7 +306,12 @@ def test_temporary_template_database_does_not_clone_a_project(monkeypatch) -> No
|
||||
assert name == "empty_conversion"
|
||||
|
||||
assert calls == [
|
||||
("copy", "tjwater_v2_template", "empty_conversion"),
|
||||
(
|
||||
"copy",
|
||||
"tjwater_v2_schema_template",
|
||||
"empty_conversion",
|
||||
"True",
|
||||
),
|
||||
("delete", "empty_conversion"),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user