aminghadersohi commented on code in PR #44152:
URL: https://github.com/apache/superset/pull/44152#discussion_r4046257621


##########
tests/unit_tests/mcp_service/chart/test_treemap_completeness.py:
##########
@@ -0,0 +1,1174 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Treemap regressions spanning native requests, query output and update 
semantics."""
+
+from contextlib import nullcontext
+from copy import deepcopy
+from decimal import Decimal
+from types import SimpleNamespace
+from typing import Any
+from unittest.mock import Mock, patch
+
+import pytest
+from pydantic import ValidationError
+
+from superset.mcp_service.chart.chart_helpers import _build_single_query_dict
+from superset.mcp_service.chart.chart_utils import (
+    map_treemap_config,
+    merge_chart_form_data,
+    resolve_treemap_update_config,
+)
+from superset.mcp_service.chart.query_result import 
normalize_chart_query_result
+from superset.mcp_service.chart.schemas import (
+    ChartError,
+    GenerateChartRequest,
+    GetChartPreviewRequest,
+    TreemapChartConfig,
+    UpdateChartPreviewRequest,
+    UpdateChartRequest,
+)
+from superset.mcp_service.chart.treemap_preview import treemap_ascii, 
treemap_vega_lite
+from superset.utils import json
+
+FORM_DATA: dict[str, Any] = {
+    "viz_type": "treemap_v2",
+    "groupby": ["region", "product"],
+    "metric": "revenue",
+    "datasource": "7__table",
+    "color_scheme": "lyftColors",
+    "row_limit": 7,
+    "sort_by_metric": False,
+    "show_labels": False,
+    "show_upper_labels": False,
+    "number_format": ",.1f",
+    "date_format": "%Y",
+    "label_type": "value",
+    "currency_format": {"symbol": "USD", "symbolPosition": "prefix"},
+    "adhoc_filters": [
+        {
+            "expressionType": "SIMPLE",
+            "clause": "WHERE",
+            "subject": "region",
+            "operator": "==",
+            "comparator": "West",
+        }
+    ],
+    "time_range": "2025-01-01 : 2026-01-01",
+    "granularity_sqla": "ds",
+    "template_params": '{"scale": 2}',
+}
+ROWS = [
+    {"region": "West", "product": "A", "revenue": 30},
+    {"region": "West", "product": "B", "revenue": 10},
+    {"region": "East", "product": "A", "revenue": 60},
+]
+
+
[email protected]("sort", [False, True])
[email protected]("limit", [None, 0, 1, 7])
+def test_hierarchy_query_order_matches_frontend(sort: bool, limit: int | None) 
-> None:
+    """Metric order has precedence, with hierarchy tie-breakers only when 
bounded."""
+    form = {**FORM_DATA, "sort_by_metric": sort, "row_limit": limit}
+    query = _build_single_query_dict(form, form["groupby"], [form["metric"]])
+    expected = ([("revenue", False)] if sort else []) + [
+        ("region", True),
+        ("product", True),
+    ]
+    assert query.get("orderby", []) == (expected if limit else [])
+
+
[email protected](
+    "metric",
+    [
+        "revenue",
+        {"name": "revenue", "saved_metric": True},
+        {
+            "expressionType": "SIMPLE",
+            "column": {"column_name": "amount"},
+            "aggregate": "SUM",
+            "label": "revenue",
+            "hasCustomLabel": True,
+        },
+        {"expressionType": "SQL", "sqlExpression": "SUM(amount)", "label": 
"revenue"},
+    ],
+)
[email protected](
+    "request_class", [GenerateChartRequest, UpdateChartPreviewRequest]
+)
+def test_native_request_roundtrip(metric: Any, request_class: Any) -> None:
+    """Native columns and all valid metric shapes survive actual request 
validation."""
+    native = {**FORM_DATA, "metric": metric, "slice_id": 1}
+    request = request_class(dataset_id=7, config=native)
+    result = map_treemap_config(request.config)
+    assert result["groupby"] == FORM_DATA["groupby"]
+    assert result["show_labels"] is False
+    assert result["number_format"] == ",.1f"
+    assert result["metric"] == "revenue" or result["metric"]["label"] == 
"revenue"
+
+
[email protected](
+    "field,value",
+    [
+        ("javascript", "alert(1)"),
+        ("groupby", []),
+        ("metric", {"expressionType": "SQL", "sqlExpression": "SUM(amount)"}),
+        ("row_limit", 10001),
+    ],
+)
+def test_native_requests_do_not_accept_unbounded_or_hostile_controls(
+    field: str, value: Any
+) -> None:
+    """Unknown executable controls and invalid native roles remain validation 
errors."""
+    with pytest.raises(ValidationError):
+        GenerateChartRequest(dataset_id=7, config={**FORM_DATA, field: value})
+
+
[email protected](
+    "patch_data",
+    [{"show_labels": True}, {"groupby": ["product", "region"], "metric": 
"revenue"}],
+)
+def test_omitted_same_viz_controls_survive(patch_data: dict[str, Any]) -> None:
+    """Partial and full-role updates preserve all omitted native controls."""
+    request = UpdateChartRequest(
+        identifier=1, config={"chart_type": "treemap_v2", **patch_data}
+    )
+    assert request.config is not None
+    config = resolve_treemap_update_config(request.config, FORM_DATA)
+    merged = merge_chart_form_data(FORM_DATA, map_treemap_config(config), 
config)
+    for key, value in FORM_DATA.items():
+        if key not in patch_data:
+            assert merged[key] == value
+    for key, value in patch_data.items():
+        assert merged[key] == value
+
+
[email protected](
+    "field",
+    [
+        "color_scheme",
+        "currency_format",
+        "time_range",
+        "granularity_sqla",
+        "template_params",
+        "filters",
+    ],
+)
+def test_nullable_clears_are_not_replaced_by_defaults(field: str) -> None:
+    """Explicit null clears native nullable controls rather than restoring 
defaults."""
+    request = UpdateChartRequest(
+        identifier=1, config={"chart_type": "treemap_v2", field: None}
+    )
+    assert request.config is not None
+    config = resolve_treemap_update_config(request.config, FORM_DATA)
+    merged = merge_chart_form_data(FORM_DATA, map_treemap_config(config), 
config)
+    assert ("adhoc_filters" if field == "filters" else field) not in merged
+
+
+def test_empty_filters_and_dataset_rebind() -> None:
+    """Rebinds retain presentation only and scrub stale query/template 
roles."""
+    config = TreemapChartConfig(
+        groupby=[{"name": "other"}], metric="other_metric", filters=[]
+    )
+    existing = {**FORM_DATA, "metrics": ["stale"], "x_axis": "stale"}
+    merged = merge_chart_form_data(
+        existing, map_treemap_config(config), config, dataset_rebind=True
+    )
+    assert merged["color_scheme"] == "lyftColors"
+    assert merged["show_labels"] is False
+    assert merged["groupby"] == ["other"]
+    assert merged["metric"] == "other_metric"
+    for key in (
+        "adhoc_filters",
+        "time_range",
+        "granularity_sqla",
+        "template_params",
+        "metrics",
+        "x_axis",
+    ):
+        assert key not in merged
+    incomplete = UpdateChartRequest(
+        identifier=1, config={"chart_type": "treemap_v2", "show_labels": True}
+    )
+    assert incomplete.config is not None
+    with pytest.raises(ValidationError):
+        resolve_treemap_update_config(incomplete.config, existing, 
dataset_rebind=True)
+
+
[email protected](
+    "result",
+    [
+        None,
+        {},
+        {"queries": []},
+        {"queries": [{}]},
+        {"queries": [{"data": {}}]},
+        {"queries": [{"data": [None]}]},
+        {"queries": [{"data": [{"region": "West", "revenue": 2}]}]},
+        {"status": "failed", "message": "Database rejected SQL"},
+        {"queries": [{"error": "SQL failed", "data": ROWS}]},
+    ],
+)
+def test_malformed_results_never_produce_success(result: Any) -> None:
+    """Missing hierarchy and malformed/error envelopes are actionable 
failures."""
+    assert isinstance(normalize_chart_query_result(result, FORM_DATA), 
ChartError)
+
+
[email protected](
+    "value", [None, True, "3", float("nan"), float("inf"), 10**400]
+)
+def test_metric_outputs_must_be_finite_numeric(value: Any) -> None:
+    """A single invalid metric fails instead of silently dropping a hierarchy 
node."""
+    rows = [*ROWS, {**ROWS[0], "revenue": value}]
+    result = normalize_chart_query_result({"queries": [{"data": rows}]}, 
FORM_DATA)
+    assert isinstance(result, ChartError)
+    assert result.error_type == "InvalidTreemapMetric"
+
+
+def test_result_validation_is_non_mutating_and_chart_specific() -> None:
+    """Other charts retain their existing interpretation of data."""
+    result = {"queries": [{"data": ROWS}]}
+    original = deepcopy(result)
+    assert normalize_chart_query_result(result, FORM_DATA) == original
+    assert result == original
+    unrelated = {"queries": [{"data": [{"x": None}]}]}
+    assert normalize_chart_query_result(unrelated, {"viz_type": "table"}) is 
unrelated
+
+
+def test_treemap_geometry_area_hierarchy_labels_and_color() -> None:
+    """Coordinates partition parents and area follows the metric, not row 
count."""
+    preview = treemap_vega_lite(
+        ROWS,
+        {
+            **FORM_DATA,
+            "currency_format": None,
+            "show_labels": True,
+            "number_format": None,
+        },
+    )
+    assert not isinstance(preview, ChartError)
+    spec = preview.specification
+    nodes = spec["data"]["values"]
+    leaves = [node for node in nodes if node["leaf"]]
+    for leaf in leaves:
+        area = (leaf["x1"] - leaf["x0"]) * (leaf["y1"] - leaf["y0"])
+        assert area / (600 * 400) == pytest.approx(leaf["value"] / 100)
+        parent = next(
+            node for node in nodes if node["path"] == leaf["path"].split(" > 
")[0]
+        )
+        assert parent["x0"] <= leaf["x0"] <= leaf["x1"] <= parent["x1"]
+        assert parent["y0"] <= leaf["y0"] <= leaf["y1"] <= parent["y1"]
+    assert spec["layer"][0]["mark"]["type"] == "rect"
+    assert spec["layer"][0]["encoding"]["color"]["scale"]["range"][0] == 
"#EA0B8C"
+    assert spec["layer"][0]["encoding"]["tooltip"][1]["title"] == "revenue"
+    assert spec["layer"][1]["mark"]["type"] == "text"
+    assert "West > A | 30" in treemap_ascii(ROWS, FORM_DATA)
+
+
[email protected](
+    "rows,override",
+    [
+        (ROWS, {"color_scheme": "unavailable"}),
+        (ROWS, {"currency_format": {"symbol": "USD"}}),
+        ([{**ROWS[0], "revenue": -1}], {}),
+        (ROWS * 334, {}),
+    ],
+)
+def test_unsupported_geometry_is_explicit(rows: Any, override: Any) -> None:
+    """Unsupported representation must not fall back to a plausible bar or 
scatter."""
+    preview = treemap_vega_lite(
+        rows, {**FORM_DATA, "currency_format": None, **override}
+    )
+    assert isinstance(preview, ChartError)
+    assert preview.error_type == "UnsupportedTreemapPreview"
+
+
[email protected]("format_name", ["ascii", "table", "vega_lite"])
+def test_saved_and_unsaved_preview_dispatch_match(format_name: str) -> None:
+    """Both preview entry paths use the Treemap representation and result 
contract."""
+    from superset.mcp_service.chart.preview_utils import 
generate_preview_from_form_data
+    from superset.mcp_service.chart.tool.get_chart_preview import (
+        ASCIIPreviewStrategy,
+        TablePreviewStrategy,
+        VegaLitePreviewStrategy,
+    )
+
+    form = {**FORM_DATA, "currency_format": None}
+    chart = SimpleNamespace(
+        id=1,
+        viz_type="treemap_v2",
+        slice_name="Treemap",
+        datasource_id=7,
+        datasource_type="table",
+        params=json.dumps(form),
+    )
+    context = SimpleNamespace(
+        queries=[SimpleNamespace(metrics=["revenue"], columns=form["groupby"])]
+    )
+    with (
+        patch(
+            "superset.commands.chart.data.get_data_command.ChartDataCommand"
+        ) as command,
+        patch(
+            
"superset.mcp_service.chart.tool.get_chart_preview.build_query_context_from_form_data",
+            return_value=context,
+        ),
+        patch(
+            
"superset.mcp_service.chart.chart_helpers.build_query_context_from_form_data",
+            return_value=context,
+        ),
+        patch("superset.extensions.db.session") as session,
+    ):
+        session.get.return_value = Mock(id=7)
+        command.return_value.run.return_value = {"queries": [{"data": ROWS}]}
+        strategy = {
+            "ascii": ASCIIPreviewStrategy,
+            "table": TablePreviewStrategy,
+            "vega_lite": VegaLitePreviewStrategy,
+        }[format_name]
+        saved = strategy(
+            chart, GetChartPreviewRequest(identifier=1, format=format_name)
+        ).generate()
+        unsaved = generate_preview_from_form_data(form, 7, format_name)
+    assert not isinstance(saved, ChartError)
+    assert not isinstance(unsaved, ChartError)
+    if format_name == "vega_lite":
+        assert saved.specification == unsaved.specification
+    elif format_name == "ascii":
+        assert "West > A | 30" in saved.ascii_content
+        assert "West > A | 30" in unsaved.ascii_content
+    else:
+        assert "region" in saved.table_data
+        assert "revenue" in unsaved.table_data
+
+
[email protected]("rebind", [False, True])
+def test_saved_update_and_update_preview_payloads_agree(rebind: bool) -> None:
+    """Both save and preview-first update paths apply the same omission 
contract."""
+    from superset.mcp_service.chart.tool.update_chart import (
+        _build_preview_form_data,
+        _build_update_payload,
+    )
+
+    config = TreemapChartConfig(
+        groupby=[{"name": "region"}], metric="revenue", show_labels=True
+    )
+    request = UpdateChartRequest(
+        identifier=1, config=config, dataset_id=8 if rebind else None
+    )
+    chart = Mock(
+        id=1, datasource_id=7, slice_name="Treemap", 
params=json.dumps(FORM_DATA)
+    )
+    with patch(
+        
"superset.mcp_service.chart.chart_utils._bind_dashboard_time_range_filter"
+    ):
+        preview = _build_preview_form_data(request, chart, 
parsed_config=config)
+        payload = _build_update_payload(request, chart, parsed_config=config)
+    saved = json.loads(payload["params"])
+    for key in (
+        "color_scheme",
+        "row_limit",
+        "sort_by_metric",
+        "groupby",
+        "metric",
+        "number_format",
+        "show_labels",
+    ):
+        assert saved[key] == preview[key]
+    assert saved["color_scheme"] == "lyftColors"
+    assert saved["show_labels"] is True
+    assert ("template_params" not in saved) == rebind
+
+
+def test_normalization_preserves_explicit_field_set_and_rejects_ambiguity() -> 
None:
+    """Canonicalization must not turn default values into explicit update 
intent."""
+    from superset.mcp_service.chart.plugins.treemap import TreemapChartPlugin
+    from superset.mcp_service.common.error_schemas import DatasetContext
+
+    context = DatasetContext(
+        id=7,
+        table_name="sales",
+        database_name="database",
+        available_columns=[{"name": "Region", "type": "STRING"}],
+        available_metrics=[{"name": "Revenue"}],
+    )
+    config = TreemapChartConfig(groupby=[{"name": "region"}], metric="revenue")
+    normalized = TreemapChartPlugin().normalize_column_refs(config, context)
+    assert normalized.groupby[0].name == "Region"
+    assert normalized.metric.name == "Revenue"
+    assert normalized.model_fields_set == config.model_fields_set
+    context.available_columns.append({"name": "REGION", "type": "STRING"})
+    with pytest.raises(ValueError, match="[Aa]mbiguous"):
+        TreemapChartPlugin().normalize_column_refs(config, context)
+
+
[email protected]
[email protected](
+    "metric",
+    [
+        "revenue",
+        {
+            "expressionType": "SIMPLE",
+            "column": {"column_name": "amount"},
+            "aggregate": "SUM",
+            "label": "revenue",
+        },
+        {"expressionType": "SQL", "sqlExpression": "SUM(amount)", "label": 
"revenue"},
+    ],
+)
[email protected]("valid_result", [False, True])
+async def test_registered_generate_chart_native_roundtrip(
+    metric: Any, valid_result: bool
+) -> None:
+    """Call the registered FastMCP tool, retaining real request and compile 
checks."""
+    from fastmcp import Client
+
+    from superset.mcp_service.app import mcp
+
+    request = GenerateChartRequest(
+        dataset_id=7,
+        config={
+            "chart_type": "treemap_v2",
+            "groupby": ["region", "product"],
+            "metric": metric,
+            "currency_format": {"symbol": "USD", "symbolPosition": "suffix"},
+        },
+        preview_formats=["url"],
+    )
+    dataset = Mock(id=7, datasource_name="sales", table_name="sales")
+    user = Mock(id=1, username="admin", roles=[], groups=[])
+    with (
+        patch("superset.mcp_service.auth.get_user_from_request", 
return_value=user),
+        patch(
+            
"superset.mcp_service.chart.validation.ValidationPipeline.validate_request_with_warnings",
+            return_value=Mock(is_valid=True, request=request, warnings={}, 
error=None),
+        ),
+        patch(
+            "superset.mcp_service.chart.chart_utils.generate_explore_link",
+            return_value="http://localhost/explore/?form_data_key=treemap";,
+        ),
+        patch("superset.daos.dataset.DatasetDAO.find_by_id", 
return_value=dataset),
+        patch(
+            
"superset.mcp_service.chart.tool.generate_chart.has_dataset_access",
+            return_value=True,
+        ),
+        patch(
+            
"superset.mcp_service.chart.chart_helpers.build_query_context_from_form_data",
+            return_value=Mock(),
+        ),
+        patch(
+            "superset.commands.chart.data.get_data_command.ChartDataCommand"
+        ) as command,
+    ):
+        command.return_value.run.return_value = {
+            "queries": [
+                {"data": ROWS if valid_result else [{"region": "West", 
"revenue": 1}]}
+            ]
+        }
+        async with Client(mcp) as client:
+            result = await client.call_tool(
+                "generate_chart",
+                {
+                    "request": {
+                        "dataset_id": 7,
+                        "config": {
+                            "viz_type": "treemap_v2",
+                            "groupby": ["region", "product"],
+                            "metric": metric,
+                            "currency_format": {
+                                "symbol": "USD",
+                                "symbolPosition": "suffix",
+                            },
+                        },
+                        "preview_formats": ["url"],
+                    }
+                },
+            )
+    data = result.structured_content
+    assert data["success"] is valid_result
+    if valid_result:
+        assert data["form_data"]["currency_format"] == {
+            "symbol": "USD",
+            "symbolPosition": "suffix",
+        }
+        assert data["form_data"]["groupby"] == ["region", "product"]
+        assert data["form_data"]["viz_type"] == "treemap_v2"
+    else:
+        assert data["error"]["error_type"] == "InvalidTreemapResult"
+    command.return_value.validate.assert_called_once()
+
+
[email protected]("number_format", [None, ",.1f"])
+def test_vega_scenegraph_renders_nested_metric_geometry(
+    number_format: str | None,
+) -> None:
+    """Compile the specification and inspect rendered rectangles, not a 
snapshot."""
+    import os
+    import shutil
+    import subprocess
+
+    if not os.environ.get("NODE_PATH"):

Review Comment:
   Confirmed — both parametrizations skip in CI, so you are right that the 
dict-shape tests alone would not catch a bad `calculate` field. Addressed in 
fcf5905 with the equivalent check you offered as the alternative, since adding 
Node packages and `NODE_PATH` to the Python-Unit workflow would mean another 
change to the very workflow file you asked me to leave alone in the next thread.
   
   `test_vega_spec_field_references_all_resolve` walks the generated 
specification with nothing but the standard library and asserts that every 
encoding `field`, and every `datum.<name>` reference inside a `filter`, 
`calculate` or `expr` string, resolves to either a key present in the emitted 
data rows or a field produced by an earlier transform in the same layer. It 
also checks each expression only calls Vega functions from an allowlist. It is 
parametrized over `number_format` and `show_labels`, which is where the 
`calculate` transforms differ.
   
   I mutation-tested it rather than trusting it: changing `datum.x0` to 
`datum.x9` in the label filter expression in `treemap_preview.py` fails the new 
test, and reverting it passes. There is also 
`test_vega_spec_field_reference_check_catches_a_bad_calculate`, which pins the 
detection behaviour itself so the check cannot silently degrade into a no-op.
   
   I left the Node scenegraph test in place — it still asserts real rendered 
geometry where Node is available, which the static check deliberately does not 
attempt.



##########
.github/workflows/superset-python-unittest.yml:
##########
@@ -43,7 +43,10 @@ jobs:
     needs: changes
     if: needs.changes.outputs.python == 'true'
     runs-on: ubuntu-26.04
-    timeout-minutes: 30
+    # The full coverage run is followed by independent SQL and semantic-layer
+    # 100% gates. Allow all three coverage-preserving phases to finish under
+    # ordinary runner variance rather than cancelling after successful tests.
+    timeout-minutes: 40

Review Comment:
   Dropped, as requested. I resolved the conflict in d08280c4 in favour of 
master, so `.github/workflows/superset-python-unittest.yml` now matches 
`origin/master` byte for byte — `git diff origin/master -- 
.github/workflows/superset-python-unittest.yml` is empty. The pytest-xdist run 
and the 45-minute timeout from #44178 are intact, and this branch no longer 
touches the file at all.
   
   I used a normal merge rather than a rebase so the existing review threads 
stay anchored to their commits.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to