mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
fix(cache): apply dashboard filters to non-legacy visualizations (#36109)
This commit is contained in:
committed by
GitHub
parent
a9fd600c52
commit
8315804c85
490
tests/unit_tests/commands/chart/warm_up_cache_test.py
Normal file
490
tests/unit_tests/commands/chart/warm_up_cache_test.py
Normal file
@@ -0,0 +1,490 @@
|
||||
# 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.
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
|
||||
from superset.models.slice import Slice
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_applies_dashboard_filters_to_non_legacy_chart(
|
||||
mock_chart_data_command, mock_get_dashboard_filters
|
||||
):
|
||||
"""Verify dashboard filters are added to query.filter for non-legacy viz"""
|
||||
# Setup: Mock dashboard filters response
|
||||
mock_get_dashboard_filters.return_value = [
|
||||
{"col": "country", "op": "in", "val": ["USA", "France"]}
|
||||
]
|
||||
|
||||
# Create a chart with non-legacy viz type
|
||||
chart = Slice(
|
||||
id=123,
|
||||
slice_name="Test Chart",
|
||||
viz_type="echarts_timeseries_bar",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
# Create mock query with empty filter list
|
||||
mock_query = Mock()
|
||||
mock_query.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
mock_qc.force = False
|
||||
|
||||
# Mock dependencies
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "echarts_timeseries_bar"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [{"error": None, "status": "success"}]
|
||||
}
|
||||
|
||||
# Execute with dashboard_id
|
||||
result = ChartWarmUpCacheCommand(chart, 42, None).run()
|
||||
|
||||
# VALIDATE: Filters were added to query.filter
|
||||
assert len(mock_query.filter) == 1, "Filter should be added to query"
|
||||
assert mock_query.filter[0] == {
|
||||
"col": "country",
|
||||
"op": "in",
|
||||
"val": ["USA", "France"],
|
||||
}, "Filter content should match dashboard filter"
|
||||
|
||||
# VALIDATE: get_dashboard_extra_filters was called correctly
|
||||
mock_get_dashboard_filters.assert_called_once_with(123, 42)
|
||||
|
||||
# VALIDATE: force flag was set
|
||||
assert mock_qc.force is True, "force should be set to True"
|
||||
|
||||
# VALIDATE: Result is correct
|
||||
assert result["chart_id"] == 123
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_no_filters_applied_without_dashboard_id(mock_chart_data_command):
|
||||
"""Verify no filters are added when dashboard_id is not provided"""
|
||||
chart = Slice(
|
||||
id=124,
|
||||
slice_name="Test Chart",
|
||||
viz_type="big_number",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
# Query starts with one existing filter
|
||||
mock_query = Mock()
|
||||
mock_query.filter = [{"col": "existing", "op": "==", "val": "filter"}]
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "big_number"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [{"error": None, "status": "success"}]
|
||||
}
|
||||
|
||||
# Execute WITHOUT dashboard_id
|
||||
ChartWarmUpCacheCommand(chart, None, None).run()
|
||||
|
||||
# VALIDATE: Filter list unchanged (no dashboard filters added)
|
||||
assert len(mock_query.filter) == 1, "Filter count should remain the same"
|
||||
assert mock_query.filter == [
|
||||
{"col": "existing", "op": "==", "val": "filter"}
|
||||
], "Existing filters should be unchanged"
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_extra_filters_parameter_takes_precedence(
|
||||
mock_chart_data_command, mock_get_dashboard_filters
|
||||
):
|
||||
"""Verify extra_filters parameter is used instead of fetching from dashboard"""
|
||||
chart = Slice(
|
||||
id=125,
|
||||
slice_name="Test Chart",
|
||||
viz_type="pie",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
mock_query = Mock()
|
||||
mock_query.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "pie"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [{"error": None, "status": "success"}]
|
||||
}
|
||||
|
||||
# Execute with extra_filters parameter
|
||||
extra_filters_json = '[{"col": "state", "op": "==", "val": "CA"}]'
|
||||
ChartWarmUpCacheCommand(chart, 42, extra_filters_json).run()
|
||||
|
||||
# VALIDATE: get_dashboard_extra_filters should NOT be called
|
||||
mock_get_dashboard_filters.assert_not_called()
|
||||
|
||||
# VALIDATE: extra_filters were parsed and applied
|
||||
assert len(mock_query.filter) == 1
|
||||
assert mock_query.filter[0] == {"col": "state", "op": "==", "val": "CA"}
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_handles_multiple_queries_in_query_context(
|
||||
mock_chart_data_command, mock_get_dashboard_filters
|
||||
):
|
||||
"""Verify filters are added to ALL queries in the query context"""
|
||||
mock_get_dashboard_filters.return_value = [
|
||||
{"col": "country", "op": "==", "val": "USA"}
|
||||
]
|
||||
|
||||
chart = Slice(
|
||||
id=126,
|
||||
slice_name="Test Chart",
|
||||
viz_type="heatmap_v2",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
# Create query context with MULTIPLE queries
|
||||
mock_query1 = Mock()
|
||||
mock_query1.filter = []
|
||||
mock_query2 = Mock()
|
||||
mock_query2.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query1, mock_query2]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "heatmap_v2"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [
|
||||
{"error": None, "status": "success"},
|
||||
{"error": None, "status": "success"},
|
||||
]
|
||||
}
|
||||
|
||||
ChartWarmUpCacheCommand(chart, 42, None).run()
|
||||
|
||||
# VALIDATE: Filters added to BOTH queries
|
||||
assert len(mock_query1.filter) == 1, "Filter should be added to query 1"
|
||||
assert len(mock_query2.filter) == 1, "Filter should be added to query 2"
|
||||
assert mock_query1.filter[0]["col"] == "country"
|
||||
assert mock_query2.filter[0]["col"] == "country"
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_handles_empty_dashboard_filters(
|
||||
mock_chart_data_command, mock_get_dashboard_filters
|
||||
):
|
||||
"""Verify graceful handling when dashboard has no filters configured"""
|
||||
# get_dashboard_extra_filters returns empty list
|
||||
mock_get_dashboard_filters.return_value = []
|
||||
|
||||
chart = Slice(
|
||||
id=127,
|
||||
slice_name="Test Chart",
|
||||
viz_type="echarts_area",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
mock_query = Mock()
|
||||
mock_query.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "echarts_area"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [{"error": None, "status": "success"}]
|
||||
}
|
||||
|
||||
ChartWarmUpCacheCommand(chart, 42, None).run()
|
||||
|
||||
# VALIDATE: No filters added (empty list case)
|
||||
assert len(mock_query.filter) == 0, (
|
||||
"No filters should be added when dashboard has no filters"
|
||||
)
|
||||
assert mock_get_dashboard_filters.called, (
|
||||
"Should still call get_dashboard_extra_filters"
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_invalid_json_in_extra_filters_raises_error(mock_chart_data_command):
|
||||
"""Verify that invalid JSON in extra_filters raises appropriate error"""
|
||||
chart = Slice(
|
||||
id=128,
|
||||
slice_name="Test Chart",
|
||||
viz_type="pie",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
# Invalid JSON string - missing closing brace
|
||||
invalid_json = '{"col": "state", "op": "==", "val": ["CA"]'
|
||||
|
||||
mock_query = Mock()
|
||||
mock_query.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "pie"}],
|
||||
):
|
||||
result = ChartWarmUpCacheCommand(chart, 42, invalid_json).run()
|
||||
|
||||
assert result["viz_error"] is not None, "Should return an error"
|
||||
assert result["chart_id"] == 128
|
||||
# JSONDecodeError messages vary across Python versions
|
||||
error_str = str(result["viz_error"]).lower()
|
||||
assert (
|
||||
"json" in error_str
|
||||
or "decode" in error_str
|
||||
or "expecting" in error_str
|
||||
or "delimiter" in error_str
|
||||
), f"Error should be a JSON decode issue: {result['viz_error']}"
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_none_query_context_raises_chart_invalid_error(mock_chart_data_command):
|
||||
"""Verify that None query context raises ChartInvalidError for non-legacy charts"""
|
||||
chart = Slice(
|
||||
id=129,
|
||||
slice_name="Test Chart",
|
||||
viz_type="echarts_timeseries",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
# Mock get_query_context to return None (chart has no query_context)
|
||||
with patch.object(chart, "get_query_context", return_value=None):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "echarts_timeseries"}],
|
||||
):
|
||||
result = ChartWarmUpCacheCommand(chart, None, None).run()
|
||||
|
||||
assert result["viz_error"] is not None, "Should return an error"
|
||||
assert result["chart_id"] == 129
|
||||
error_str = str(result["viz_error"]).lower()
|
||||
assert "query context" in error_str, (
|
||||
f"Error should mention query context: {result['viz_error']}"
|
||||
)
|
||||
assert "not exist" in error_str, (
|
||||
f"Error should mention not exist: {result['viz_error']}"
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
|
||||
def test_legacy_chart_without_datasource_raises_error():
|
||||
"""Verify that legacy chart without datasource raises ChartInvalidError"""
|
||||
chart = Slice(
|
||||
id=130,
|
||||
slice_name="Legacy Chart",
|
||||
viz_type="table",
|
||||
datasource_id=None,
|
||||
datasource_type=None,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
type(chart), "datasource", new_callable=lambda: property(lambda self: None)
|
||||
):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "table"}],
|
||||
):
|
||||
result = ChartWarmUpCacheCommand(chart, None, None).run()
|
||||
|
||||
assert result["viz_error"] is not None, "Should return an error"
|
||||
assert result["chart_id"] == 130
|
||||
error_str = str(result["viz_error"]).lower()
|
||||
assert "datasource" in error_str, (
|
||||
f"Error should mention datasource: {result['viz_error']}"
|
||||
)
|
||||
assert "not exist" in error_str, (
|
||||
f"Error should mention not exist: {result['viz_error']}"
|
||||
)
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
|
||||
@patch("superset.commands.chart.warm_up_cache.get_viz")
|
||||
@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
|
||||
@patch("superset.commands.chart.warm_up_cache.g")
|
||||
def test_legacy_chart_warm_up_with_dashboard(
|
||||
mock_g, mock_get_viz, mock_get_dashboard_filters
|
||||
):
|
||||
"""Test successful legacy chart warm-up with dashboard filters"""
|
||||
mock_get_dashboard_filters.return_value = [
|
||||
{"col": "country", "op": "==", "val": "USA"}
|
||||
]
|
||||
|
||||
chart = Slice(
|
||||
id=131,
|
||||
slice_name="Legacy Table",
|
||||
viz_type="table",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
mock_datasource = Mock()
|
||||
mock_datasource.type = "table"
|
||||
mock_datasource.id = 1
|
||||
|
||||
mock_viz = Mock()
|
||||
mock_viz.get_payload.return_value = {"errors": None, "status": "success"}
|
||||
mock_get_viz.return_value = mock_viz
|
||||
|
||||
with patch.object(
|
||||
type(chart),
|
||||
"datasource",
|
||||
new_callable=lambda: property(lambda self: mock_datasource),
|
||||
):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "table"}],
|
||||
):
|
||||
result = ChartWarmUpCacheCommand(chart, 42, None).run()
|
||||
|
||||
assert result["chart_id"] == 131
|
||||
assert result["viz_error"] is None
|
||||
assert result["viz_status"] == "success"
|
||||
assert hasattr(mock_g, "form_data") is False
|
||||
mock_get_dashboard_filters.assert_called_once_with(131, 42)
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.get_viz")
|
||||
@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
|
||||
@patch("superset.commands.chart.warm_up_cache.g")
|
||||
def test_legacy_chart_warm_up_without_dashboard(mock_g, mock_get_viz):
|
||||
"""Test successful legacy chart warm-up without dashboard"""
|
||||
chart = Slice(
|
||||
id=134,
|
||||
slice_name="Legacy Table",
|
||||
viz_type="table",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
mock_datasource = Mock()
|
||||
mock_datasource.type = "table"
|
||||
mock_datasource.id = 1
|
||||
|
||||
mock_viz = Mock()
|
||||
mock_viz.get_payload.return_value = {"errors": None, "status": "success"}
|
||||
mock_get_viz.return_value = mock_viz
|
||||
|
||||
with patch.object(
|
||||
type(chart),
|
||||
"datasource",
|
||||
new_callable=lambda: property(lambda self: mock_datasource),
|
||||
):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "table"}],
|
||||
):
|
||||
result = ChartWarmUpCacheCommand(chart, None, None).run()
|
||||
|
||||
assert result["chart_id"] == 134
|
||||
assert result["viz_error"] is None
|
||||
assert result["viz_status"] == "success"
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
|
||||
def test_non_legacy_chart_returns_first_error(mock_chart_data_command):
|
||||
"""Test that first query error is returned when multiple queries exist"""
|
||||
chart = Slice(
|
||||
id=132,
|
||||
slice_name="Chart with Error",
|
||||
viz_type="echarts_timeseries",
|
||||
datasource_id=1,
|
||||
datasource_type="table",
|
||||
)
|
||||
|
||||
mock_query = Mock()
|
||||
mock_query.filter = []
|
||||
mock_qc = Mock()
|
||||
mock_qc.queries = [mock_query]
|
||||
|
||||
with patch.object(chart, "get_query_context", return_value=mock_qc):
|
||||
with patch(
|
||||
"superset.commands.chart.warm_up_cache.get_form_data",
|
||||
return_value=[{"viz_type": "echarts_timeseries"}],
|
||||
):
|
||||
mock_chart_data_command.return_value.run.return_value = {
|
||||
"queries": [
|
||||
{"error": "Database connection failed", "status": "failed"},
|
||||
{"error": None, "status": "success"},
|
||||
]
|
||||
}
|
||||
|
||||
result = ChartWarmUpCacheCommand(chart, None, None).run()
|
||||
|
||||
assert result["chart_id"] == 132
|
||||
assert result["viz_error"] == "Database connection failed"
|
||||
assert result["viz_status"] == "failed"
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.db")
|
||||
def test_validate_with_integer_chart_id(mock_db):
|
||||
"""Test validation when passing integer chart ID instead of Slice object"""
|
||||
chart = Slice(id=133, slice_name="Test Chart")
|
||||
mock_db.session.query.return_value.filter_by.return_value.scalar.return_value = (
|
||||
chart
|
||||
)
|
||||
|
||||
command = ChartWarmUpCacheCommand(133, None, None)
|
||||
command.validate()
|
||||
|
||||
assert command._chart_or_id == chart
|
||||
mock_db.session.query.assert_called_once()
|
||||
|
||||
|
||||
@patch("superset.commands.chart.warm_up_cache.db")
|
||||
def test_validate_with_nonexistent_chart_id(mock_db):
|
||||
"""Test validation raises error when chart ID does not exist"""
|
||||
from superset.commands.chart.exceptions import WarmUpCacheChartNotFoundError
|
||||
|
||||
mock_db.session.query.return_value.filter_by.return_value.scalar.return_value = None
|
||||
|
||||
command = ChartWarmUpCacheCommand(99999, None, None)
|
||||
|
||||
with pytest.raises(WarmUpCacheChartNotFoundError):
|
||||
command.validate()
|
||||
Reference in New Issue
Block a user