mirror of
https://github.com/apache/superset.git
synced 2026-04-08 02:45:22 +00:00
491 lines
18 KiB
Python
491 lines
18 KiB
Python
# 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()
|