Add tests for audits and flavor utilities, update .gitignore, and enhance CPU data handling
This commit is contained in:
@@ -11,6 +11,9 @@ from dashboard.prometheus_utils.query import query_prometheus
|
||||
def convert_cpu_data(data: list):
|
||||
metrics = []
|
||||
|
||||
if not data:
|
||||
return pandas.DataFrame(columns=["host", "cpu_usage"])
|
||||
|
||||
for entry in data:
|
||||
for t, val in entry["values"]:
|
||||
metrics.append({
|
||||
|
||||
43
dashboard/tests/test_audits.py
Normal file
43
dashboard/tests/test_audits.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Tests for dashboard.openstack_utils.audits."""
|
||||
from django.test import TestCase
|
||||
|
||||
from dashboard.openstack_utils.audits import convert_cpu_data
|
||||
|
||||
|
||||
class ConvertCpuDataTest(TestCase):
|
||||
"""Tests for convert_cpu_data."""
|
||||
|
||||
def test_aggregates_cpu_usage_per_host(self):
|
||||
data = [
|
||||
{
|
||||
"metric": {"host": "compute-0", "instanceName": "inst1"},
|
||||
"values": [[1000, "10.0"], [1001, "20.0"]],
|
||||
},
|
||||
{
|
||||
"metric": {"host": "compute-0", "instanceName": "inst2"},
|
||||
"values": [[1000, "5.0"]],
|
||||
},
|
||||
{
|
||||
"metric": {"host": "compute-1", "instanceName": "inst3"},
|
||||
"values": [[1000, "30.0"]],
|
||||
},
|
||||
]
|
||||
result = convert_cpu_data(data)
|
||||
self.assertIn("host", result.columns)
|
||||
self.assertIn("cpu_usage", result.columns)
|
||||
hosts = result["host"].tolist()
|
||||
self.assertEqual(len(hosts), 2)
|
||||
self.assertIn("compute-0", hosts)
|
||||
self.assertIn("compute-1", hosts)
|
||||
# compute-0: (10+20)/2 for ts 1000 and 5 for ts 1000 -> groupby host,timestamp sum -> then groupby host mean
|
||||
# For compute-0: two timestamps 1000 (10+5=15) and 1001 (20). Mean over timestamps = (15+20)/2 = 17.5
|
||||
# For compute-1: one value 30
|
||||
by_host = result.set_index("host")["cpu_usage"]
|
||||
self.assertAlmostEqual(by_host["compute-0"], 17.5)
|
||||
self.assertAlmostEqual(by_host["compute-1"], 30.0)
|
||||
|
||||
def test_empty_data_returns_empty_dataframe_with_columns(self):
|
||||
result = convert_cpu_data([])
|
||||
self.assertIn("host", result.columns)
|
||||
self.assertIn("cpu_usage", result.columns)
|
||||
self.assertEqual(len(result), 0)
|
||||
64
dashboard/tests/test_flavor.py
Normal file
64
dashboard/tests/test_flavor.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Tests for dashboard.openstack_utils.flavor."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from dashboard.openstack_utils.flavor import get_flavor_list
|
||||
|
||||
|
||||
def make_mock_server(flavor_id):
|
||||
"""Create a mock server object with flavor['id']."""
|
||||
s = MagicMock()
|
||||
s.flavor = {"id": flavor_id}
|
||||
return s
|
||||
|
||||
|
||||
class GetFlavorListTest(TestCase):
|
||||
"""Tests for get_flavor_list."""
|
||||
|
||||
def test_returns_first_second_third_common_flavor_keys(self):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.compute.servers.return_value = [
|
||||
make_mock_server("m1.small"),
|
||||
make_mock_server("m1.small"),
|
||||
make_mock_server("m1.medium"),
|
||||
]
|
||||
result = get_flavor_list(connection=mock_conn)
|
||||
self.assertIn("first_common_flavor", result)
|
||||
self.assertIn("second_common_flavor", result)
|
||||
self.assertIn("third_common_flavor", result)
|
||||
|
||||
def test_most_common_flavor_first(self):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.compute.servers.return_value = [
|
||||
make_mock_server("m1.large"),
|
||||
make_mock_server("m1.small"),
|
||||
make_mock_server("m1.small"),
|
||||
make_mock_server("m1.small"),
|
||||
]
|
||||
result = get_flavor_list(connection=mock_conn)
|
||||
self.assertEqual(result["first_common_flavor"]["name"], "m1.small")
|
||||
self.assertEqual(result["first_common_flavor"]["count"], 3)
|
||||
self.assertEqual(result["second_common_flavor"]["name"], "m1.large")
|
||||
self.assertEqual(result["second_common_flavor"]["count"], 1)
|
||||
self.assertEqual(result["third_common_flavor"]["name"], "—")
|
||||
self.assertEqual(result["third_common_flavor"]["count"], 0)
|
||||
|
||||
def test_empty_servers_uses_placeholder_for_all(self):
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.compute.servers.return_value = []
|
||||
result = get_flavor_list(connection=mock_conn)
|
||||
placeholder = {"name": "—", "count": 0}
|
||||
self.assertEqual(result["first_common_flavor"], placeholder)
|
||||
self.assertEqual(result["second_common_flavor"], placeholder)
|
||||
self.assertEqual(result["third_common_flavor"], placeholder)
|
||||
|
||||
def test_skips_servers_without_flavor_id(self):
|
||||
mock_conn = MagicMock()
|
||||
s_with_id = make_mock_server("m1.small")
|
||||
s_without = MagicMock()
|
||||
s_without.flavor = {} # no 'id'
|
||||
mock_conn.compute.servers.return_value = [s_with_id, s_without]
|
||||
result = get_flavor_list(connection=mock_conn)
|
||||
self.assertEqual(result["first_common_flavor"]["name"], "m1.small")
|
||||
self.assertEqual(result["first_common_flavor"]["count"], 1)
|
||||
62
dashboard/tests/test_prometheus_query.py
Normal file
62
dashboard/tests/test_prometheus_query.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Tests for dashboard.prometheus_utils.query."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from dashboard.prometheus_utils.query import query_prometheus
|
||||
|
||||
|
||||
class QueryPrometheusTest(TestCase):
|
||||
"""Tests for query_prometheus."""
|
||||
|
||||
@patch("dashboard.prometheus_utils.query.requests.get")
|
||||
def test_single_result_returns_value_string(self, mock_get):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"data": {
|
||||
"result": [
|
||||
{"value": ["1234567890", "42"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = query_prometheus("some_query")
|
||||
|
||||
self.assertEqual(result, "42")
|
||||
mock_response.raise_for_status.assert_called_once()
|
||||
mock_get.assert_called_once()
|
||||
call_kw = mock_get.call_args
|
||||
self.assertIn("params", call_kw.kwargs)
|
||||
self.assertEqual(call_kw.kwargs["params"]["query"], "some_query")
|
||||
|
||||
@patch("dashboard.prometheus_utils.query.requests.get")
|
||||
def test_multiple_results_returns_full_result_list(self, mock_get):
|
||||
mock_response = MagicMock()
|
||||
result_list = [
|
||||
{"metric": {"host": "h1"}, "value": ["1", "10"]},
|
||||
{"metric": {"host": "h2"}, "value": ["1", "20"]},
|
||||
]
|
||||
mock_response.json.return_value = {"data": {"result": result_list}}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
result = query_prometheus("vector_query")
|
||||
|
||||
self.assertEqual(result, result_list)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
@patch("dashboard.prometheus_utils.query.requests.get")
|
||||
def test_uses_prometheus_url_from_settings(self, mock_get):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"data": {"result": [{"value": ["0", "1"]}]}}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
query_prometheus("test")
|
||||
|
||||
mock_get.assert_called_once()
|
||||
args, kwargs = mock_get.call_args
|
||||
url = args[0] if args else kwargs.get("url", "")
|
||||
self.assertIn("/api/v1/query", url)
|
||||
145
dashboard/tests/test_views.py
Normal file
145
dashboard/tests/test_views.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Tests for dashboard.views."""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.core.cache import cache
|
||||
|
||||
from dashboard.views import index, collect_context
|
||||
|
||||
|
||||
def _minimal_render_context(region_name="test", first_flavor_name="f1", vm_count=1):
|
||||
"""Context with all keys the index.html template expects."""
|
||||
return {
|
||||
"region": {"name": region_name, "hosts_total": 1},
|
||||
"pcpu": {"total": 1, "usage": 0, "free": 1, "used_percentage": 0},
|
||||
"vcpu": {"total": 2, "allocated": 1, "free": 1, "allocated_percentage": 50, "overcommit_ratio": 1, "overcommit_max": 2},
|
||||
"pram": {"total": 1024**3, "usage": 0, "free": 1024**3, "used_percentage": 0},
|
||||
"vram": {"total": 1024**3, "allocated": 0, "free": 1024**3, "allocated_percentage": 0, "overcommit_ratio": 0, "overcommit_max": 1},
|
||||
"vm": {"count": vm_count, "active": vm_count, "stopped": 0, "avg_cpu": 1, "avg_ram": 0, "density": float(vm_count)},
|
||||
"flavors": {
|
||||
"first_common_flavor": {"name": first_flavor_name, "count": vm_count},
|
||||
"second_common_flavor": {"name": "—", "count": 0},
|
||||
"third_common_flavor": {"name": "—", "count": 0},
|
||||
},
|
||||
"audits": [],
|
||||
}
|
||||
|
||||
|
||||
class IndexViewTest(TestCase):
|
||||
"""Tests for the index view."""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@patch("dashboard.views.settings")
|
||||
def test_index_use_mock_data_returns_200_and_mock_context(self, mock_settings):
|
||||
mock_settings.USE_MOCK_DATA = True
|
||||
mock_settings.DASHBOARD_CACHE_TTL = 120
|
||||
request = self.factory.get("/")
|
||||
response = index(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Mock context contains mock-region and flavors; render uses index.html
|
||||
content = response.content.decode()
|
||||
self.assertIn("mock-region", content)
|
||||
self.assertIn("m1.small", content)
|
||||
|
||||
@patch("dashboard.views.collect_context")
|
||||
@patch("dashboard.views.get_mock_context")
|
||||
@patch("dashboard.views.settings")
|
||||
def test_index_with_real_path_uses_cache_and_collect_context(
|
||||
self, mock_settings, mock_get_mock_context, mock_collect_context
|
||||
):
|
||||
mock_settings.USE_MOCK_DATA = False
|
||||
mock_settings.DASHBOARD_CACHE_TTL = 120
|
||||
mock_collect_context.return_value = _minimal_render_context(
|
||||
region_name="cached-region", first_flavor_name="f1"
|
||||
)
|
||||
cache.clear()
|
||||
request = self.factory.get("/")
|
||||
with patch.object(cache, "get", return_value=None):
|
||||
with patch.object(cache, "set") as mock_cache_set:
|
||||
response = index(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_collect_context.assert_called_once()
|
||||
mock_cache_set.assert_called_once()
|
||||
mock_get_mock_context.assert_not_called()
|
||||
content = response.content.decode()
|
||||
self.assertIn("cached-region", content)
|
||||
self.assertIn("f1", content)
|
||||
|
||||
@patch("dashboard.views.collect_context")
|
||||
@patch("dashboard.views.settings")
|
||||
def test_index_with_cache_hit_does_not_call_collect_context(
|
||||
self, mock_settings, mock_collect_context
|
||||
):
|
||||
mock_settings.USE_MOCK_DATA = False
|
||||
mock_settings.DASHBOARD_CACHE_TTL = 120
|
||||
cached = _minimal_render_context(region_name="from-cache", first_flavor_name="cached-flavor", vm_count=2)
|
||||
cache.clear()
|
||||
cache.set("dashboard_context", cached, timeout=120)
|
||||
request = self.factory.get("/")
|
||||
response = index(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
mock_collect_context.assert_not_called()
|
||||
content = response.content.decode()
|
||||
self.assertIn("from-cache", content)
|
||||
|
||||
|
||||
class CollectContextTest(TestCase):
|
||||
"""Tests for collect_context with mocked dependencies."""
|
||||
|
||||
def _make_mock_connection(self, region_name="test-region"):
|
||||
conn = MagicMock()
|
||||
conn._compute_region = region_name
|
||||
return conn
|
||||
|
||||
@patch("dashboard.views._fetch_prometheus_metrics")
|
||||
@patch("dashboard.views.get_audits")
|
||||
@patch("dashboard.views.get_flavor_list")
|
||||
@patch("dashboard.views.get_connection")
|
||||
def test_collect_context_structure_and_calculation(
|
||||
self, mock_get_connection, mock_get_flavor_list, mock_get_audits, mock_fetch_metrics
|
||||
):
|
||||
mock_get_connection.return_value = self._make_mock_connection("my-region")
|
||||
mock_get_flavor_list.return_value = {
|
||||
"first_common_flavor": {"name": "m1.small", "count": 5},
|
||||
"second_common_flavor": {"name": "—", "count": 0},
|
||||
"third_common_flavor": {"name": "—", "count": 0},
|
||||
}
|
||||
mock_get_audits.return_value = [
|
||||
{
|
||||
"migrations": [],
|
||||
"host_labels": ["h0", "h1"],
|
||||
"cpu_current": [30.0, 40.0],
|
||||
"cpu_projected": [35.0, 35.0],
|
||||
}
|
||||
]
|
||||
mock_fetch_metrics.return_value = {
|
||||
"hosts_total": 2,
|
||||
"pcpu_total": 8,
|
||||
"pcpu_usage": 2.5,
|
||||
"vcpu_allocated": 16,
|
||||
"vcpu_overcommit_max": 2.0,
|
||||
"pram_total": 32 * 1024**3,
|
||||
"pram_usage": 8 * 1024**3,
|
||||
"vram_allocated": 24 * 1024**3,
|
||||
"vram_overcommit_max": 1.5,
|
||||
"vm_count": 4,
|
||||
"vm_active": 4,
|
||||
}
|
||||
|
||||
context = collect_context()
|
||||
|
||||
self.assertEqual(context["region"]["name"], "my-region")
|
||||
self.assertEqual(context["region"]["hosts_total"], 2)
|
||||
self.assertEqual(context["pcpu"]["total"], 8)
|
||||
self.assertEqual(context["pcpu"]["usage"], 2.5)
|
||||
self.assertEqual(context["vcpu"]["total"], 8 * 2.0) # pcpu_total * vcpu_overcommit_max
|
||||
self.assertEqual(context["vcpu"]["allocated"], 16)
|
||||
self.assertEqual(context["vram"]["total"], 32 * 1024**3 * 1.5)
|
||||
self.assertEqual(context["flavors"]["first_common_flavor"]["name"], "m1.small")
|
||||
self.assertEqual(len(context["audits"]), 1)
|
||||
# Serialized for JS
|
||||
import json
|
||||
self.assertIsInstance(context["audits"][0]["migrations"], str)
|
||||
self.assertEqual(json.loads(context["audits"][0]["host_labels"]), ["h0", "h1"])
|
||||
Reference in New Issue
Block a user