Files
superset2/tests/unit_tests/cli/thumbnails_test.py
2026-07-09 19:26:41 -07:00

80 lines
3.0 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 click.testing import CliRunner
from pytest_mock import MockerFixture
from superset.cli import thumbnails
class _FakeModel:
"""Stand-in for a Dashboard/Slice that detaches mid-run.
Once ``detached`` is set, ``str()`` raises -- mimicking the
DetachedInstanceError that SQLAlchemy raises when the session that
loaded the instance has been closed/expired.
"""
def __init__(self, pk: int, label: str) -> None:
self.id: int = pk
self._label: str = label
self.detached: bool = False
def __str__(self) -> str:
if self.detached:
raise RuntimeError("Instance is not bound to a Session (simulated)")
return self._label
def test_compute_thumbnails_survives_detached_instances(
mocker: MockerFixture, app_context: None
) -> None:
"""Regression test for the DetachedInstanceError in compute-thumbnails.
Computing a thumbnail can close/expire the session, detaching the ORM
instances. The command must read each model's id and label *before* the
compute loop, so building the progress message never touches a detached
instance even when several models are processed.
"""
models = [_FakeModel(1, "Dashboard A"), _FakeModel(2, "Dashboard B")]
query = mocker.MagicMock()
query.filter.return_value = query
query.all.return_value = models
db_mock = mocker.patch("superset.cli.thumbnails.db")
db_mock.session.query.return_value = query
def _detach_session(_url: object, _model_id: int, force: bool) -> None:
# Computing a thumbnail expires the session -> every instance detaches.
for model in models:
model.detached = True
cache_dashboard = mocker.patch(
"superset.tasks.thumbnails.cache_dashboard_thumbnail",
side_effect=_detach_session,
)
result = CliRunner().invoke(thumbnails.compute_thumbnails, ["-d"])
assert result.exit_code == 0, result.output
# Both dashboards were processed despite the simulated detachment...
assert cache_dashboard.call_count == 2
cache_dashboard.assert_any_call(None, 1, force=False)
cache_dashboard.assert_any_call(None, 2, force=False)
# ...and their labels were captured up front for the progress output.
assert "Dashboard A" in result.output
assert "Dashboard B" in result.output