520 lines
19 KiB
Python
520 lines
19 KiB
Python
# #region Test.DashboardTopology [C:3] [TYPE Module] [SEMANTICS test,topology,dashboard,superset]
|
|
# @BRIEF Comprehensive tests for _topology.py — chart name/params, topology building, layout parsing.
|
|
# @RELATION BINDS_TO -> [DashboardTopology]
|
|
# @TEST_EDGE: chart_name_fallback -> Falls back to title or "Chart id" when slice_name missing
|
|
# @TEST_EDGE: params_string_parse -> String params parsed, invalid string returns {}
|
|
# @TEST_EDGE: describe_empty_optional -> No metrics/groupby/filters returns empty string
|
|
# @TEST_EDGE: resolve_child_string -> String ref resolved from pos_json, missing ref returns None
|
|
# @TEST_EDGE: topology_without_grid -> No GRID/ROOT nodes falls back to flat iteration
|
|
# @TEST_EDGE: build_topology_string_posjson -> position_json as string is parsed
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
# ── _chart_name ──
|
|
|
|
class TestChartName:
|
|
"""Verify _chart_name."""
|
|
|
|
def test_slice_name_used(self):
|
|
from src.plugins.llm_analysis._topology import _chart_name
|
|
assert _chart_name({"slice_name": "Revenue"}) == "Revenue"
|
|
|
|
def test_title_fallback(self):
|
|
from src.plugins.llm_analysis._topology import _chart_name
|
|
assert _chart_name({"title": "Chart Title"}) == "Chart Title"
|
|
|
|
def test_slice_name_overrides_title(self):
|
|
from src.plugins.llm_analysis._topology import _chart_name
|
|
assert _chart_name({"slice_name": "Actual", "title": "Fallback"}) == "Actual"
|
|
|
|
def test_id_fallback(self):
|
|
from src.plugins.llm_analysis._topology import _chart_name
|
|
result = _chart_name({"id": 42})
|
|
assert result == "Chart 42"
|
|
|
|
def test_empty_dict(self):
|
|
from src.plugins.llm_analysis._topology import _chart_name
|
|
result = _chart_name({})
|
|
assert result == "Chart None"
|
|
|
|
|
|
# ── _chart_params ──
|
|
|
|
class TestChartParams:
|
|
"""Verify _chart_params."""
|
|
|
|
def test_dict_params(self):
|
|
from src.plugins.llm_analysis._topology import _chart_params
|
|
result = _chart_params({"params": {"metrics": ["count"]}})
|
|
assert result == {"metrics": ["count"]}
|
|
|
|
def test_string_params(self):
|
|
from src.plugins.llm_analysis._topology import _chart_params
|
|
result = _chart_params({"params": '{"metrics": ["count"]}'})
|
|
assert result == {"metrics": ["count"]}
|
|
|
|
def test_invalid_string_params(self):
|
|
from src.plugins.llm_analysis._topology import _chart_params
|
|
result = _chart_params({"params": "not-json"})
|
|
assert result == {}
|
|
|
|
def test_empty_params(self):
|
|
from src.plugins.llm_analysis._topology import _chart_params
|
|
assert _chart_params({}) == {}
|
|
|
|
def test_none_params(self):
|
|
from src.plugins.llm_analysis._topology import _chart_params
|
|
# When params is None, returns None (params value as-is)
|
|
assert _chart_params({"params": None}) is None
|
|
|
|
|
|
# ── _describe_chart_dataset ──
|
|
|
|
class TestDescribeChartDataset:
|
|
"""Verify _describe_chart_dataset."""
|
|
|
|
def test_with_datasource_id(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_dataset
|
|
result = _describe_chart_dataset({"datasource_id": 5}, {})
|
|
assert "Dataset: 5" in result
|
|
|
|
def test_datasource_id_in_params(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_dataset
|
|
result = _describe_chart_dataset({}, {"datasource_id": 10})
|
|
assert "Dataset: 10" in result
|
|
|
|
def test_no_datasource(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_dataset
|
|
assert _describe_chart_dataset({}, {}) == ""
|
|
|
|
|
|
# ── _describe_chart_metrics ──
|
|
|
|
class TestDescribeChartMetrics:
|
|
"""Verify _describe_chart_metrics."""
|
|
|
|
def test_dict_metrics(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
result = _describe_chart_metrics({"metrics": [{"label": "SUM(revenue)"}]})
|
|
assert "SUM(revenue)" in result
|
|
|
|
def test_string_metrics(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
result = _describe_chart_metrics({"metrics": ["count"]})
|
|
assert "count" in result
|
|
|
|
def test_dict_metric_expression_fallback(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
result = _describe_chart_metrics({"metrics": [{"expression": "SUM(x)"}]})
|
|
assert "SUM(x)" in result
|
|
|
|
def test_empty_metrics(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
assert _describe_chart_metrics({"metrics": []}) == ""
|
|
|
|
def test_missing_metrics(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
assert _describe_chart_metrics({}) == ""
|
|
|
|
def test_truncated_to_three(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_metrics
|
|
metrics = [str(i) for i in range(10)]
|
|
result = _describe_chart_metrics({"metrics": metrics})
|
|
assert len(result.split(",")) <= 4 # "Metrics: " prefix + 3 items max
|
|
|
|
|
|
# ── _describe_chart_groupby ──
|
|
|
|
class TestDescribeChartGroupby:
|
|
"""Verify _describe_chart_groupby."""
|
|
|
|
def test_with_groupby(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_groupby
|
|
result = _describe_chart_groupby({"groupby": ["category", "region"]})
|
|
assert "category" in result
|
|
assert "region" in result
|
|
|
|
def test_truncated_to_three(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_groupby
|
|
result = _describe_chart_groupby({"groupby": ["a", "b", "c", "d"]})
|
|
assert "d" not in result
|
|
|
|
def test_empty_groupby(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_groupby
|
|
assert _describe_chart_groupby({"groupby": []}) == ""
|
|
|
|
def test_missing_groupby(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_groupby
|
|
assert _describe_chart_groupby({}) == ""
|
|
|
|
|
|
# ── _describe_chart_filters ──
|
|
|
|
class TestDescribeChartFilters:
|
|
"""Verify _describe_chart_filters."""
|
|
|
|
def test_full_filter(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_filters
|
|
result = _describe_chart_filters({
|
|
"adhoc_filters": [
|
|
{"subject": "revenue", "operator": ">", "comparator": "1000"}
|
|
]
|
|
})
|
|
assert "revenue" in result
|
|
assert ">" in result
|
|
|
|
def test_filter_without_subject_comparator(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_filters
|
|
result = _describe_chart_filters({
|
|
"adhoc_filters": [
|
|
{"clause": "having"}
|
|
]
|
|
})
|
|
assert "having" in result
|
|
|
|
def test_empty_filters(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_filters
|
|
assert _describe_chart_filters({"adhoc_filters": []}) == ""
|
|
|
|
def test_missing_filters(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_filters
|
|
assert _describe_chart_filters({}) == ""
|
|
|
|
def test_non_dict_filter(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart_filters
|
|
result = _describe_chart_filters({"adhoc_filters": ["raw_string"]})
|
|
assert result == "" # not a dict, no clause key
|
|
|
|
|
|
# ── _describe_chart ──
|
|
|
|
class TestDescribeChart:
|
|
"""Verify _describe_chart orchestration."""
|
|
|
|
def test_describe_integration(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart
|
|
chart = {
|
|
"slice_name": "Sales",
|
|
"viz_type": "bar",
|
|
"params": {"metrics": [{"label": "SUM(rev)"}]},
|
|
}
|
|
result = _describe_chart(chart)
|
|
assert 'Chart "Sales" (bar)' in result
|
|
assert "SUM(rev)" in result
|
|
|
|
def test_describe_minimal(self):
|
|
from src.plugins.llm_analysis._topology import _describe_chart
|
|
result = _describe_chart({"id": 99})
|
|
assert 'Chart "Chart 99" (unknown)' in result
|
|
|
|
|
|
# ── _sort_chart_key ──
|
|
|
|
class TestSortChartKey:
|
|
"""Verify _sort_chart_key."""
|
|
|
|
def test_numeric_key(self):
|
|
from src.plugins.llm_analysis._topology import _sort_chart_key
|
|
result = _sort_chart_key(("42", {}))
|
|
assert result == (0, 42)
|
|
|
|
def test_string_key(self):
|
|
from src.plugins.llm_analysis._topology import _sort_chart_key
|
|
result = _sort_chart_key(("chart_1", {}))
|
|
assert result == (1, "chart_1")
|
|
|
|
def test_non_numeric_key(self):
|
|
from src.plugins.llm_analysis._topology import _sort_chart_key
|
|
result = _sort_chart_key(("abc", {}))
|
|
assert result == (1, "abc")
|
|
|
|
|
|
# ── _resolve_child ──
|
|
|
|
class TestResolveChild:
|
|
"""Verify _resolve_child."""
|
|
|
|
def test_dict_ref_returns_as_is(self):
|
|
from src.plugins.llm_analysis._topology import _resolve_child
|
|
ref = {"type": "CHART"}
|
|
assert _resolve_child(ref, {}) is ref
|
|
|
|
def test_string_ref_found(self):
|
|
from src.plugins.llm_analysis._topology import _resolve_child
|
|
pos_json = {"key1": {"type": "CHART"}}
|
|
result = _resolve_child("key1", pos_json)
|
|
assert result == {"type": "CHART"}
|
|
|
|
def test_string_ref_not_found(self):
|
|
from src.plugins.llm_analysis._topology import _resolve_child
|
|
assert _resolve_child("missing", {}) is None
|
|
|
|
|
|
# ── _process_tab ──
|
|
|
|
class TestProcessTab:
|
|
"""Verify _process_tab."""
|
|
|
|
def test_tab_with_children(self):
|
|
from src.plugins.llm_analysis._topology import _process_tab
|
|
tab_node = {
|
|
"type": "TAB",
|
|
"meta": {"text": "Revenue Tab"},
|
|
"children": [
|
|
{"type": "CHART", "meta": {"chartId": 1}},
|
|
]
|
|
}
|
|
result = _process_tab(tab_node, depth=1, pos_json={}, chart_lookup={"1": {"slice_name": "Rev Chart"}})
|
|
assert len(result) == 2
|
|
assert "Revenue Tab" in result[0]
|
|
assert "Rev Chart" in result[1]
|
|
|
|
def test_tab_meta_label_fallback(self):
|
|
from src.plugins.llm_analysis._topology import _process_tab
|
|
tab_node = {"type": "TAB", "meta": {"label": "Tab Label"}, "children": []}
|
|
result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
|
|
assert "Tab Label" in result[0]
|
|
|
|
def test_tab_empty_meta(self):
|
|
from src.plugins.llm_analysis._topology import _process_tab
|
|
tab_node = {"children": []}
|
|
result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
|
|
assert "Tab" in result[0]
|
|
|
|
|
|
# ── _process_chart ──
|
|
|
|
class TestProcessChart:
|
|
"""Verify _process_chart."""
|
|
|
|
def test_chart_found_in_lookup(self):
|
|
from src.plugins.llm_analysis._topology import _process_chart
|
|
child = {"meta": {"chartId": 5}}
|
|
chart_lookup = {"5": {"slice_name": "Test Chart", "viz_type": "line"}}
|
|
result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
|
|
assert len(result) == 1
|
|
assert "Test Chart" in result[0]
|
|
|
|
def test_chart_not_in_lookup(self):
|
|
from src.plugins.llm_analysis._topology import _process_chart
|
|
child = {"meta": {"chartId": 99}}
|
|
result = _process_chart(child, depth=0, chart_lookup={})
|
|
assert result == []
|
|
|
|
def test_chart_with_slice_id(self):
|
|
from src.plugins.llm_analysis._topology import _process_chart
|
|
child = {"meta": {"slice_id": 10}}
|
|
chart_lookup = {"10": {"slice_name": "Slice Chart"}}
|
|
result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
|
|
assert len(result) == 1
|
|
|
|
def test_chart_missing_meta(self):
|
|
from src.plugins.llm_analysis._topology import _process_chart
|
|
result = _process_chart({}, depth=0, chart_lookup={"1": {}})
|
|
assert result == []
|
|
|
|
|
|
# ── _process_children ──
|
|
|
|
class TestProcessChildren:
|
|
"""Verify _process_children."""
|
|
|
|
def test_tab_child(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
parent = {
|
|
"children": [
|
|
{"type": "TAB", "meta": {"text": "Tab1"}, "children": []}
|
|
]
|
|
}
|
|
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
|
|
assert len(result) == 1
|
|
assert "Tab1" in result[0]
|
|
|
|
def test_row_child(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
parent = {
|
|
"children": [
|
|
{"type": "ROW", "children": [
|
|
{"type": "CHART", "meta": {"chartId": 1}}
|
|
]}
|
|
]
|
|
}
|
|
chart_lookup = {"1": {"slice_name": "C1"}}
|
|
result = _process_children(parent, depth=0, pos_json={}, chart_lookup=chart_lookup)
|
|
assert len(result) == 1
|
|
|
|
def test_unknown_type_skipped(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
parent = {"children": [{"type": "HEADER", "meta": {}}]}
|
|
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
|
|
assert result == []
|
|
|
|
def test_child_ref_resolved(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
parent = {"children": ["ref_key"]}
|
|
pos_json = {"ref_key": {"type": "CHART", "meta": {"chartId": 1}}}
|
|
chart_lookup = {"1": {"slice_name": "Ref C"}}
|
|
result = _process_children(parent, depth=0, pos_json=pos_json, chart_lookup=chart_lookup)
|
|
assert len(result) == 1
|
|
|
|
def test_child_ref_not_resolved(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
parent = {"children": ["missing_key"]}
|
|
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
|
|
assert result == []
|
|
|
|
def test_non_dict_parent(self):
|
|
from src.plugins.llm_analysis._topology import _process_children
|
|
result = _process_children("not_a_dict", depth=0, pos_json={}, chart_lookup={})
|
|
assert result == []
|
|
|
|
|
|
# ── _extract_topology_children ──
|
|
|
|
class TestExtractTopologyChildren:
|
|
"""Verify _extract_topology_children."""
|
|
|
|
def test_grid_nodes(self):
|
|
from src.plugins.llm_analysis._topology import _extract_topology_children
|
|
pos_json = {
|
|
"GRID_ID": {"type": "GRID", "children": [
|
|
{"type": "CHART", "meta": {"chartId": 1}}
|
|
]}
|
|
}
|
|
chart_lookup = {"1": {"slice_name": "GChart"}}
|
|
result = _extract_topology_children(pos_json, chart_lookup)
|
|
assert len(result) >= 1
|
|
|
|
def test_no_grid_fallback(self):
|
|
from src.plugins.llm_analysis._topology import _extract_topology_children
|
|
pos_json = {
|
|
"tab1": {"type": "TAB", "children": []},
|
|
"DASHBOARD_VERSION_KEY": "v2",
|
|
}
|
|
result = _extract_topology_children(pos_json, chart_lookup={})
|
|
assert isinstance(result, list)
|
|
|
|
def test_no_grid_skips_version_key(self):
|
|
from src.plugins.llm_analysis._topology import _extract_topology_children
|
|
pos_json = {"DASHBOARD_VERSION_KEY": "v2"}
|
|
result = _extract_topology_children(pos_json, chart_lookup={})
|
|
assert result == []
|
|
|
|
def test_empty_position_json(self):
|
|
from src.plugins.llm_analysis._topology import _extract_topology_children
|
|
assert _extract_topology_children({}, {}) == []
|
|
|
|
|
|
# ── build_dashboard_topology (integration) ──
|
|
|
|
class TestBuildDashboardTopology:
|
|
"""Verify build_dashboard_topology full function."""
|
|
|
|
def test_happy_path(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"id": 1,
|
|
"dashboard_title": "Sales Overview",
|
|
"position_json": {
|
|
"ROOT_ID": {
|
|
"type": "ROOT",
|
|
"children": [
|
|
{"type": "CHART", "meta": {"chartId": 10}}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
charts = [{"id": 10, "slice_name": "Revenue", "viz_type": "big_number"}]
|
|
result = build_dashboard_topology(dashboard, charts)
|
|
assert 'Dashboard: "Sales Overview"' in result
|
|
assert "Revenue" in result
|
|
assert "Charts: 1 total" in result
|
|
|
|
def test_title_fallback(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {"title": "Alt Title", "position_json": {}}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Alt Title" in result
|
|
|
|
def test_no_title_fallback(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {"id": 5, "position_json": {}}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Dashboard 5" in result
|
|
|
|
def test_position_json_as_string(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "Test",
|
|
"position_json": json.dumps({
|
|
"ROOT_ID": {"type": "ROOT", "children": []}
|
|
}),
|
|
}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Test" in result
|
|
|
|
def test_position_json_invalid_string(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "Broken",
|
|
"position_json": "not-valid-json{{",
|
|
}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Broken" in result # gracefully handles parse failure
|
|
|
|
def test_position_json_not_dict(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "NoPos",
|
|
"position_json": [],
|
|
}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "NoPos" in result
|
|
|
|
def test_chart_lookup_with_slice_id(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "Test",
|
|
"position_json": {},
|
|
}
|
|
charts = [{"slice_id": 1, "slice_name": "Slice Chart", "viz_type": "table"}]
|
|
result = build_dashboard_topology(dashboard, charts)
|
|
assert "Slice Chart" in result
|
|
|
|
def test_topology_parse_warning(self):
|
|
"""Exception during _extract_topology_children is caught and noted."""
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "Err",
|
|
"position_json": {
|
|
"ROOT_ID": {"type": "ROOT", "children": ["missing"]}
|
|
},
|
|
}
|
|
# missing key in position_json triggers no error since _resolve_child returns None
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Err" in result
|
|
|
|
def test_charts_sorted_by_key(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {
|
|
"dashboard_title": "Sorted",
|
|
"position_json": {},
|
|
}
|
|
charts = [
|
|
{"id": 10, "slice_name": "Z Chart"},
|
|
{"id": 5, "slice_name": "A Chart"},
|
|
]
|
|
result = build_dashboard_topology(dashboard, charts)
|
|
# Both appear
|
|
assert "A Chart" in result
|
|
assert "Z Chart" in result
|
|
|
|
def test_empty_charts(self):
|
|
from src.plugins.llm_analysis._topology import build_dashboard_topology
|
|
dashboard = {"dashboard_title": "Empty", "position_json": {}}
|
|
result = build_dashboard_topology(dashboard, [])
|
|
assert "Empty" in result
|
|
assert "Charts: 0 total" not in result # no chart_lookup when charts empty
|
|
# #endregion Test.DashboardTopology
|