fix syntax error and it pass the test case

This commit is contained in:
Rainy21
2026-04-28 14:59:12 -04:00
parent 71a9858ddd
commit b59bc0749f
12 changed files with 608 additions and 61 deletions
+213
View File
@@ -0,0 +1,213 @@
"""
tests/test_nodes.py
Unit tests for fetch_nodes().
No real cluster needed — all Kubernetes API calls are mocked.
Run:
pytest tests/test_nodes.py -v
"""
from datetime import datetime, timezone, timedelta
from unittest.mock import MagicMock, patch
import pytest
from app.k8s_client import fetch_nodes, _parse_millicores, _parse_mebibytes
# ── Fixtures ───────────────────────────────────────────────────────────────────
def _make_condition(type_: str, status: str):
c = MagicMock()
c.type = type_
c.status = status
return c
def _make_node(
name="node-1",
ready_status="True",
cpu="4",
memory="8058752Ki",
alloc_memory="7542528Ki",
os_image="Ubuntu 22.04.3 LTS",
kubelet_version="v1.28.5+k3s1",
labels=None,
created_days_ago=10,
mem_pressure="False",
disk_pressure="False",
pid_pressure="False",
):
node = MagicMock()
node.metadata.name = name
node.metadata.labels = labels if labels is not None else {"node-role.kubernetes.io/control-plane": "true"}
node.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta(
days=created_days_ago
)
node.status.conditions = [
_make_condition("Ready", ready_status),
_make_condition("MemoryPressure", mem_pressure),
_make_condition("DiskPressure", disk_pressure),
_make_condition("PIDPressure", pid_pressure),
]
node.status.capacity = {"cpu": cpu, "memory": memory}
node.status.allocatable = {"cpu": cpu, "memory": alloc_memory}
node.status.node_info.os_image = os_image
node.status.node_info.kubelet_version = kubelet_version
return node
@pytest.fixture
def mock_core(monkeypatch):
"""Patch get_core() so no real kubeconfig is needed."""
mock = MagicMock()
monkeypatch.setattr("app.k8s_client.get_core", lambda: mock)
return mock
# ── Parser unit tests (no mock needed) ────────────────────────────────────────
class TestParsers:
def test_millicores_with_suffix(self):
assert _parse_millicores("250m") == pytest.approx(0.25)
def test_millicores_without_suffix(self):
assert _parse_millicores("4") == pytest.approx(4.0)
def test_mebibytes_mi(self):
assert _parse_mebibytes("512Mi") == pytest.approx(512.0)
def test_mebibytes_gi(self):
assert _parse_mebibytes("1Gi") == pytest.approx(1024.0)
def test_mebibytes_ki(self):
assert _parse_mebibytes("1024Ki") == pytest.approx(1.0)
def test_mebibytes_raw_bytes(self):
assert _parse_mebibytes("1048576") == pytest.approx(1.0)
# ── fetch_nodes() tests ────────────────────────────────────────────────────────
class TestFetchNodes:
def test_returns_list_of_dicts(self, mock_core):
mock_core.list_node.return_value.items = [_make_node()]
result = fetch_nodes()
assert isinstance(result, list)
assert isinstance(result[0], dict)
def test_ready_node(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(ready_status="True")]
node = fetch_nodes()[0]
assert node["ready"] is True
def test_not_ready_node(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(ready_status="False")]
node = fetch_nodes()[0]
assert node["ready"] is False
def test_ready_is_bool_not_string(self, mock_core):
"""Regression: status comes back as string 'True' — must be cast to bool."""
mock_core.list_node.return_value.items = [_make_node(ready_status="True")]
node = fetch_nodes()[0]
assert node["ready"] is True
assert not isinstance(node["ready"], str)
def test_node_name(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(name="k3s-worker-1")]
assert fetch_nodes()[0]["name"] == "k3s-worker-1"
def test_control_plane_role(self, mock_core):
mock_core.list_node.return_value.items = [
_make_node(labels={"node-role.kubernetes.io/control-plane": "true"})
]
assert "control-plane" in fetch_nodes()[0]["roles"]
def test_worker_role_fallback(self, mock_core):
"""Nodes with no role label should fall back to 'worker'."""
mock_core.list_node.return_value.items = [_make_node(labels={})]
assert fetch_nodes()[0]["roles"] == "worker"
def test_memory_parsed_to_float(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(memory="8058752Ki")]
node = fetch_nodes()[0]
assert isinstance(node["mem_cap_mi"], float)
assert node["mem_cap_mi"] > 0
def test_cpu_parsed_to_float(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(cpu="4")]
assert fetch_nodes()[0]["cpu_cap"] == pytest.approx(4.0)
def test_age_days(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(created_days_ago=5)]
assert fetch_nodes()[0]["age"] == "5d"
def test_age_hours(self, mock_core):
node = _make_node()
node.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta(
hours=3
)
mock_core.list_node.return_value.items = [node]
assert fetch_nodes()[0]["age"] == "3h"
def test_pressure_fields_present(self, mock_core):
mock_core.list_node.return_value.items = [_make_node()]
node = fetch_nodes()[0]
assert "mem_pressure" in node
assert "disk_pressure" in node
assert "pid_pressure" in node
def test_memory_pressure_true(self, mock_core):
mock_core.list_node.return_value.items = [_make_node(mem_pressure="True")]
assert fetch_nodes()[0]["mem_pressure"] == "True"
def test_multiple_nodes(self, mock_core):
mock_core.list_node.return_value.items = [
_make_node(name="node-1"),
_make_node(name="node-2"),
]
result = fetch_nodes()
assert len(result) == 2
assert {r["name"] for r in result} == {"node-1", "node-2"}
def test_empty_cluster(self, mock_core):
mock_core.list_node.return_value.items = []
assert fetch_nodes() == []
def test_null_conditions_handled(self, mock_core):
"""Nodes mid-join can have conditions=None — must not raise."""
node = _make_node()
node.status.conditions = None
mock_core.list_node.return_value.items = [node]
result = fetch_nodes()
assert result[0]["ready"] is False
def test_null_labels_handled(self, mock_core):
node = _make_node()
node.metadata.labels = None
mock_core.list_node.return_value.items = [node]
assert fetch_nodes()[0]["roles"] == "worker"
def test_all_keys_present(self, mock_core):
"""Every dict must have the same stable set of keys."""
mock_core.list_node.return_value.items = [_make_node()]
node = fetch_nodes()[0]
expected_keys = {
"name",
"ready",
"roles",
"age",
"cpu_cap",
"mem_cap_mi",
"mem_alloc_mi",
"os",
"kubelet_version",
"mem_pressure",
"disk_pressure",
"pid_pressure",
}
assert expected_keys == set(node.keys())
+248
View File
@@ -0,0 +1,248 @@
"""
tests/test_pods.py
Unit tests for fetch_pods().
No real cluster needed — all Kubernetes API calls are mocked.
Run:
pytest tests/test_pods.py -v
"""
from datetime import datetime, timezone, timedelta
from unittest.mock import MagicMock
import pytest
from app.k8s_client import fetch_pods
# ── Fixtures ───────────────────────────────────────────────────────────────────
def _make_container_status(restart_count=0, ready=True):
cs = MagicMock()
cs.restart_count = restart_count
cs.ready = ready
return cs
def _make_pod(
name="pod-1",
namespace="default",
phase="Running",
ready_status="True",
restarts=0,
node_name="k3s-worker-1",
created_minutes_ago=60,
container_statuses=None,
):
pod = MagicMock()
pod.metadata.name = name
pod.metadata.namespace = namespace
pod.metadata.creation_timestamp = datetime.now(timezone.utc) - timedelta(
minutes=created_minutes_ago
)
pod.status.phase = phase
pod.status.conditions = [
_cond("Ready", ready_status),
_cond("ContainersReady", ready_status),
_cond("PodScheduled", "True"),
_cond("Initialized", "True"),
]
pod.status.container_statuses = container_statuses or [
_make_container_status(restart_count=restarts)
]
pod.spec.node_name = node_name
return pod
def _cond(type_: str, status: str):
c = MagicMock()
c.type = type_
c.status = status
return c
@pytest.fixture
def mock_core(monkeypatch):
mock = MagicMock()
monkeypatch.setattr("app.k8s_client.get_core", lambda: mock)
return mock
# ── fetch_pods() — return shape ────────────────────────────────────────────────
class TestFetchPodsShape:
def test_returns_list_of_dicts(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [_make_pod()]
result = fetch_pods("all")
assert isinstance(result, list)
assert isinstance(result[0], dict)
def test_all_keys_present(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [_make_pod()]
pod = fetch_pods("all")[0]
assert set(pod.keys()) == {
"namespace",
"name",
"ready",
"phase",
"restarts",
"node",
"age",
}
def test_empty_result(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = []
assert fetch_pods("all") == []
# ── fetch_pods() — namespace routing ──────────────────────────────────────────
class TestNamespaceRouting:
def test_all_namespace_uses_cluster_wide_call(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = []
fetch_pods("all")
mock_core.list_pod_for_all_namespaces.assert_called_once()
mock_core.list_namespaced_pod.assert_not_called()
def test_specific_namespace_uses_namespaced_call(self, mock_core):
mock_core.list_namespaced_pod.return_value.items = []
fetch_pods("kube-system")
mock_core.list_namespaced_pod.assert_called_once_with(namespace="kube-system")
mock_core.list_pod_for_all_namespaces.assert_not_called()
def test_namespace_preserved_in_result(self, mock_core):
mock_core.list_namespaced_pod.return_value.items = [
_make_pod(namespace="kube-system")
]
pod = fetch_pods("kube-system")[0]
assert pod["namespace"] == "kube-system"
# ── fetch_pods() — readiness ───────────────────────────────────────────────────
class TestPodReadiness:
def test_ready_pod(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(ready_status="True")
]
assert fetch_pods("all")[0]["ready"] is True
def test_not_ready_pod(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(ready_status="False")
]
assert fetch_pods("all")[0]["ready"] is False
def test_ready_is_bool_not_string(self, mock_core):
"""Regression: condition.status is the string 'True', must be cast to bool."""
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(ready_status="True")
]
assert fetch_pods("all")[0]["ready"] is True
assert not isinstance(fetch_pods("all")[0]["ready"], str)
def test_running_but_not_ready(self, mock_core):
"""phase=Running does NOT mean ready. This is the key gotcha."""
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(phase="Running", ready_status="False")
]
pod = fetch_pods("all")[0]
assert pod["phase"] == "Running"
assert pod["ready"] is False # not the same thing
# ── fetch_pods() — phase ───────────────────────────────────────────────────────
class TestPodPhase:
@pytest.mark.parametrize("phase", ["Running", "Pending", "Failed", "Succeeded"])
def test_phase_preserved(self, mock_core, phase):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(phase=phase)
]
assert fetch_pods("all")[0]["phase"] == phase
def test_none_phase_becomes_unknown(self, mock_core):
pod = _make_pod()
pod.status.phase = None
mock_core.list_pod_for_all_namespaces.return_value.items = [pod]
assert fetch_pods("all")[0]["phase"] == "Unknown"
# ── fetch_pods() — restarts ────────────────────────────────────────────────────
class TestPodRestarts:
def test_zero_restarts(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(restarts=0)
]
assert fetch_pods("all")[0]["restarts"] == 0
def test_nonzero_restarts(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(restarts=7)
]
assert fetch_pods("all")[0]["restarts"] == 7
def test_restarts_summed_across_containers(self, mock_core):
"""Multi-container pods sum restarts from all containers."""
pod = _make_pod(
container_statuses=[
_make_container_status(restart_count=3),
_make_container_status(restart_count=5),
]
)
mock_core.list_pod_for_all_namespaces.return_value.items = [pod]
assert fetch_pods("all")[0]["restarts"] == 8
def test_null_container_statuses_handled(self, mock_core):
"""Pending pods have no container_statuses yet — must not raise."""
pod = _make_pod()
pod.status.container_statuses = None
mock_core.list_pod_for_all_namespaces.return_value.items = [pod]
assert fetch_pods("all")[0]["restarts"] == 0
# ── fetch_pods() — edge cases ──────────────────────────────────────────────────
class TestPodEdgeCases:
def test_null_conditions_handled(self, mock_core):
pod = _make_pod()
pod.status.conditions = None
mock_core.list_pod_for_all_namespaces.return_value.items = [pod]
result = fetch_pods("all")
assert result[0]["ready"] is False
def test_null_node_name(self, mock_core):
"""Pending pods are not yet scheduled to a node."""
pod = _make_pod()
pod.spec.node_name = None
mock_core.list_pod_for_all_namespaces.return_value.items = [pod]
assert fetch_pods("all")[0]["node"] == "—"
def test_multiple_pods(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(name="pod-a", namespace="default"),
_make_pod(name="pod-b", namespace="kube-system"),
]
result = fetch_pods("all")
assert len(result) == 2
assert {r["name"] for r in result} == {"pod-a", "pod-b"}
def test_age_minutes(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(created_minutes_ago=25)
]
assert fetch_pods("all")[0]["age"] == "25m"
def test_age_hours(self, mock_core):
mock_core.list_pod_for_all_namespaces.return_value.items = [
_make_pod(created_minutes_ago=180)
]
assert fetch_pods("all")[0]["age"] == "3h"