"""Named sizes: storing them, and what happens to a node that asks for one.""" import pytest from fastapi.testclient import TestClient from sqlmodel import Session, select from fluksio.core.config import settings from fluksio.core.db import seed_flavors from fluksio.models import Flavor def test_an_instance_starts_with_sizes_to_pick_from( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: response = client.get( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers ) assert response.status_code == 200 names = {row["name"] for row in response.json()["data"]} assert {"small", "medium", "large", "gpu-small"} <= names def test_seeding_leaves_an_instance_that_has_its_own_alone(db: Session) -> None: """Re-adding a size somebody deliberately removed is an argument to avoid.""" before = {row.name for row in db.exec(select(Flavor)).all()} seed_flavors(db) assert {row.name for row in db.exec(select(Flavor)).all()} == before def test_a_size_can_be_added_changed_and_removed( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: created = client.post( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers, json={"name": "gpu-big", "cpus": 16, "gpus": 4, "ram": 65536}, ) assert created.status_code == 200 assert created.json()["gpus"] == 4 again = client.post( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers, json={"name": "gpu-big", "cpus": 1}, ) assert again.status_code == 409 changed = client.patch( f"{settings.API_V1_STR}/flavors/gpu-big", headers=superuser_token_headers, json={"cpus": 32}, ) assert changed.status_code == 200 # Only what was sent: the rest of the size is untouched. assert (changed.json()["cpus"], changed.json()["gpus"]) == (32, 4) removed = client.delete( f"{settings.API_V1_STR}/flavors/gpu-big", headers=superuser_token_headers ) assert removed.status_code == 200 listed = client.get( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers ).json() assert "gpu-big" not in {row["name"] for row in listed["data"]} def test_changing_a_size_is_a_superusers( client: TestClient, normal_user_token_headers: dict[str, str] ) -> None: """Reading is open — the node panel offers them — and writing is not.""" assert ( client.get( f"{settings.API_V1_STR}/flavors/", headers=normal_user_token_headers ).status_code == 200 ) refused = client.post( f"{settings.API_V1_STR}/flavors/", headers=normal_user_token_headers, json={"name": "sneaky", "cpus": 99}, ) assert refused.status_code == 403 def test_a_name_nobody_could_type_is_refused( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: refused = client.post( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers, json={"name": "GPU Big!", "cpus": 1}, ) assert refused.status_code == 422 def test_a_flow_cannot_name_a_size_that_does_not_exist( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: """Caught at the save, which covers the canvas and `fluksio sync` at once.""" response = client.put( f"{settings.API_V1_STR}/flows/flavor_check", headers=superuser_token_headers, json={ "name": "flavor_check", "version": 1, "nodes": [ {"id": "fit", "type": "python", "resources": {"flavor": "imaginary"}} ], }, ) assert response.status_code == 422 assert "imaginary" in str(response.json()["detail"]) def test_a_size_a_node_still_asks_for_is_not_deleted( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: client.post( f"{settings.API_V1_STR}/flavors/", headers=superuser_token_headers, json={"name": "in-use", "cpus": 2}, ) client.put( f"{settings.API_V1_STR}/flows/flavor_user", headers=superuser_token_headers, json={ "name": "flavor_user", "version": 1, "nodes": [ {"id": "fit", "type": "python", "resources": {"flavor": "in-use"}} ], }, ) try: refused = client.delete( f"{settings.API_V1_STR}/flavors/in-use", headers=superuser_token_headers ) assert refused.status_code == 409 assert "flavor_user.fit" in refused.json()["detail"] finally: client.delete( f"{settings.API_V1_STR}/flows/flavor_user", headers=superuser_token_headers ) client.delete( f"{settings.API_V1_STR}/flavors/in-use", headers=superuser_token_headers ) def test_a_named_size_resolves_to_the_numbers_stored_for_it(db: Session) -> None: from fluksio.flow.resources import UnknownFlavor, resolve_flavor from fluksio.flow.schemas import Resources resolved = resolve_flavor(Resources(flavor="gpu-small", duration_s=60)) seeded = db.get(Flavor, "gpu-small") assert (resolved.cpus, resolved.gpus, resolved.ram) == ( seeded.cpus, seeded.gpus, seeded.ram, ) # What the node said about itself survives; the flavor only sizes it. assert resolved.duration_s == 60 assert resolved.flavor is None # Read at build time, so editing a flavor edits what the next run gets. seeded.cpus = 12 db.add(seeded) db.commit() assert resolve_flavor(Resources(flavor="gpu-small")).cpus == 12 with pytest.raises(UnknownFlavor, match="does not exist"): resolve_flavor(Resources(flavor="never-stored")) def test_declaring_nothing_never_looks_anything_up() -> None: """The common node pays nothing for a feature it does not use.""" from fluksio.flow.resources import resolve_flavor from fluksio.flow.schemas import Resources wanted = Resources(cpus=2) assert resolve_flavor(wanted) is wanted