mirror of
https://github.com/apache/superset.git
synced 2026-07-11 17:25:31 +00:00
401 lines
15 KiB
Python
401 lines
15 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 typing import Any, Optional
|
|
from unittest import mock
|
|
|
|
|
|
def _fake_app(config: Optional[dict[str, Any]] = None) -> mock.MagicMock:
|
|
"""Build a stand-in for ``current_app`` with a controllable config dict."""
|
|
base: dict[str, Any] = {"WEBDRIVER_TYPE": "chrome"}
|
|
if config:
|
|
base.update(config)
|
|
app: mock.MagicMock = mock.MagicMock()
|
|
app.config = base
|
|
return app
|
|
|
|
|
|
def test_cache_warmup_unknown_strategy(app_context: None) -> None:
|
|
"""An unknown strategy name returns an explanatory message and warms nothing."""
|
|
from superset.tasks.cache import cache_warmup
|
|
|
|
with mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd:
|
|
result: dict[str, list[str]] | str = cache_warmup("does_not_exist")
|
|
|
|
assert result == "No strategy does_not_exist found!"
|
|
mock_wd.assert_not_called()
|
|
|
|
|
|
def test_cache_warmup_missing_config(app_context: None) -> None:
|
|
"""Without SUPERSET_CACHE_WARMUP_USER the task fails fast before any WebDriver."""
|
|
from superset.tasks.cache import cache_warmup
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.current_app",
|
|
_fake_app({"SUPERSET_CACHE_WARMUP_USER": None}),
|
|
),
|
|
mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
|
|
):
|
|
result: dict[str, list[str]] | str = cache_warmup("dummy")
|
|
|
|
assert isinstance(result, str)
|
|
assert "SUPERSET_CACHE_WARMUP_USER is not configured" in result
|
|
mock_wd.assert_not_called()
|
|
|
|
|
|
def test_cache_warmup_user_not_found(app_context: None) -> None:
|
|
"""A configured-but-missing warmup user fails fast before any WebDriver."""
|
|
from superset.tasks.cache import cache_warmup
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.current_app",
|
|
_fake_app({"SUPERSET_CACHE_WARMUP_USER": "bot"}),
|
|
),
|
|
mock.patch("superset.tasks.cache.security_manager") as mock_sm,
|
|
mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
|
|
):
|
|
mock_sm.find_user = mock.MagicMock(return_value=None)
|
|
result: dict[str, list[str]] | str = cache_warmup("dummy")
|
|
|
|
assert isinstance(result, str)
|
|
assert "not found" in result
|
|
mock_sm.find_user.assert_called_once_with(username="bot")
|
|
mock_wd.assert_not_called()
|
|
|
|
|
|
def test_cache_warmup_happy_path(app_context: None) -> None:
|
|
"""Each URL is screenshotted as the configured user and the driver is destroyed."""
|
|
from superset.tasks.cache import cache_warmup
|
|
|
|
urls: list[str] = ["http://localhost/dash/1", "http://localhost/dash/2"]
|
|
user: mock.MagicMock = mock.MagicMock()
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.current_app",
|
|
_fake_app({"SUPERSET_CACHE_WARMUP_USER": "bot"}),
|
|
),
|
|
mock.patch("superset.tasks.cache.security_manager") as mock_sm,
|
|
mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
|
|
mock.patch("superset.tasks.cache.DummyStrategy.get_urls", return_value=urls),
|
|
):
|
|
mock_sm.find_user = mock.MagicMock(return_value=user)
|
|
driver: mock.MagicMock = mock_wd.return_value
|
|
result: dict[str, list[str]] | str = cache_warmup("dummy")
|
|
|
|
assert result == {"success": urls, "errors": []}
|
|
mock_wd.assert_called_once_with("chrome", user=user)
|
|
assert driver.get_screenshot.call_count == len(urls)
|
|
driver.destroy.assert_called_once_with()
|
|
|
|
|
|
def test_cache_warmup_collects_errors_and_destroys(app_context: None) -> None:
|
|
"""A failing URL is recorded as an error and cleanup still runs in finally."""
|
|
from superset.tasks.cache import cache_warmup
|
|
|
|
urls: list[str] = ["http://localhost/dash/ok", "http://localhost/dash/boom"]
|
|
user: mock.MagicMock = mock.MagicMock()
|
|
|
|
def side_effect(url: str, _element: str) -> None:
|
|
if url.endswith("boom"):
|
|
raise Exception("screenshot failed")
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.current_app",
|
|
_fake_app({"SUPERSET_CACHE_WARMUP_USER": "bot"}),
|
|
),
|
|
mock.patch("superset.tasks.cache.security_manager") as mock_sm,
|
|
mock.patch("superset.tasks.cache.WebDriverSelenium") as mock_wd,
|
|
mock.patch("superset.tasks.cache.DummyStrategy.get_urls", return_value=urls),
|
|
):
|
|
mock_sm.find_user = mock.MagicMock(return_value=user)
|
|
driver: mock.MagicMock = mock_wd.return_value
|
|
driver.get_screenshot.side_effect = side_effect
|
|
result: dict[str, list[str]] | str = cache_warmup("dummy")
|
|
|
|
assert result == {
|
|
"success": ["http://localhost/dash/ok"],
|
|
"errors": ["http://localhost/dash/boom"],
|
|
}
|
|
driver.destroy.assert_called_once_with()
|
|
|
|
|
|
def test_native_filter_options_strategy_returns_tasks_for_eligible_filters() -> None:
|
|
"""Eligible native filters are converted into cache warm-up tasks."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 10
|
|
filter_configs: list[dict[str, Any]] = [{"id": "filter-1"}, {"id": "filter-2"}]
|
|
form_data: list[dict[str, Any]] = [
|
|
{"groupby": ["country"]},
|
|
{"groupby": ["state"]},
|
|
]
|
|
query_contexts: list[mock.MagicMock] = [mock.MagicMock(), mock.MagicMock()]
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=dashboard,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.get_eligible_native_filters",
|
|
return_value=filter_configs,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_form_data",
|
|
side_effect=form_data,
|
|
) as mock_build_form_data,
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
side_effect=query_contexts,
|
|
) as mock_build_query_context,
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert len(tasks) == 2
|
|
assert [task.query_context for task in tasks] == query_contexts
|
|
assert [task.dashboard_id for task in tasks] == [10, 10]
|
|
assert [task.native_filter_id for task in tasks] == ["filter-1", "filter-2"]
|
|
assert mock_build_form_data.call_count == 2
|
|
assert mock_build_query_context.call_count == 2
|
|
|
|
|
|
def test_native_filter_options_strategy_skips_missing_dashboard() -> None:
|
|
"""Missing dashboards are skipped without raising."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=None,
|
|
),
|
|
mock.patch("superset.tasks.cache.get_eligible_native_filters") as mock_filters,
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert tasks == []
|
|
mock_filters.assert_not_called()
|
|
|
|
|
|
def test_native_filter_options_strategy_skips_when_form_data_is_none() -> None:
|
|
"""Filters without form data are skipped without raising."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 10
|
|
filter_configs: list[dict[str, Any]] = [{"id": "filter-1"}, {"id": "filter-2"}]
|
|
query_context: mock.MagicMock = mock.MagicMock()
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=dashboard,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.get_eligible_native_filters",
|
|
return_value=filter_configs,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_form_data",
|
|
side_effect=[None, {"groupby": ["country"]}],
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
return_value=query_context,
|
|
) as mock_build_query_context,
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].query_context == query_context
|
|
mock_build_query_context.assert_called_once_with({"groupby": ["country"]})
|
|
|
|
|
|
def test_native_filter_options_strategy_skips_when_query_context_is_none() -> None:
|
|
"""Filters without a query context are skipped without raising."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 10
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=dashboard,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.get_eligible_native_filters",
|
|
return_value=[{"id": "filter-1"}],
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_form_data",
|
|
return_value={"groupby": ["country"]},
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
return_value=None,
|
|
),
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert tasks == []
|
|
|
|
|
|
def test_native_filter_options_strategy_uses_real_filter_helpers() -> None:
|
|
"""Eligible filter config is handled by real helper functions."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
from superset.utils import json
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 10
|
|
dashboard.json_metadata = json.dumps(
|
|
{
|
|
"native_filter_configuration": [
|
|
"bad-entry",
|
|
{
|
|
"id": "filter-1",
|
|
"filterType": "filter_select",
|
|
"type": "NATIVE_FILTER",
|
|
"targets": [{"datasetId": 7, "column": {"name": "country"}}],
|
|
"adhoc_filters": [
|
|
{
|
|
"expressionType": "SIMPLE",
|
|
"subject": "region",
|
|
"operator": "==",
|
|
"comparator": "EMEA",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "range-filter",
|
|
"filterType": "filter_range",
|
|
"type": "NATIVE_FILTER",
|
|
"targets": [{"datasetId": 7, "column": {"name": "value"}}],
|
|
},
|
|
]
|
|
}
|
|
)
|
|
query_context: mock.MagicMock = mock.MagicMock()
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=dashboard,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
return_value=query_context,
|
|
) as mock_build_query_context,
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].dashboard_id == 10
|
|
assert tasks[0].native_filter_id == "filter-1"
|
|
assert tasks[0].query_context == query_context
|
|
mock_build_query_context.assert_called_once()
|
|
form_data: dict[str, Any] = mock_build_query_context.call_args.args[0]
|
|
assert form_data["datasource"] == "7__table"
|
|
assert form_data["groupby"] == ["country"]
|
|
assert form_data["adhoc_filters"] == [
|
|
{
|
|
"expressionType": "SIMPLE",
|
|
"subject": "region",
|
|
"operator": "==",
|
|
"comparator": "EMEA",
|
|
}
|
|
]
|
|
|
|
|
|
def test_native_filter_options_strategy_skips_filter_helper_exceptions() -> None:
|
|
"""A failing filter config is skipped while remaining filters are processed."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 10
|
|
filter_configs: list[dict[str, Any]] = [{"missing_id": True}, {"id": "filter-2"}]
|
|
query_context: mock.MagicMock = mock.MagicMock()
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
return_value=dashboard,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.get_eligible_native_filters",
|
|
return_value=filter_configs,
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_form_data",
|
|
side_effect=[RuntimeError("boom"), {"groupby": ["country"]}],
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
return_value=query_context,
|
|
),
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(dashboard_ids=[10]).get_tasks()
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].native_filter_id == "filter-2"
|
|
assert tasks[0].query_context == query_context
|
|
|
|
|
|
def test_native_filter_options_strategy_skips_dashboard_lookup_exceptions() -> None:
|
|
"""A dashboard-level failure is caught and does not stop other dashboard ids."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy
|
|
|
|
dashboard: mock.MagicMock = mock.MagicMock()
|
|
dashboard.id = 11
|
|
query_context: mock.MagicMock = mock.MagicMock()
|
|
|
|
with (
|
|
mock.patch(
|
|
"superset.tasks.cache.DashboardDAO.find_by_id",
|
|
side_effect=[RuntimeError("boom"), dashboard],
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.get_eligible_native_filters",
|
|
return_value=[{"id": "filter-1"}],
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_form_data",
|
|
return_value={"groupby": ["country"]},
|
|
),
|
|
mock.patch(
|
|
"superset.tasks.cache.build_native_filter_option_query_context",
|
|
return_value=query_context,
|
|
),
|
|
):
|
|
tasks: list[Any] = NativeFilterOptionsStrategy(
|
|
dashboard_ids=[10, 11]
|
|
).get_tasks()
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].dashboard_id == 11
|
|
assert tasks[0].native_filter_id == "filter-1"
|
|
|
|
|
|
def test_native_filter_options_strategy_registered() -> None:
|
|
"""The native filter options strategy is registered by name."""
|
|
from superset.tasks.cache import NativeFilterOptionsStrategy, strategy_registry
|
|
|
|
assert strategy_registry["native_filter_options"] is NativeFilterOptionsStrategy
|