Files
superset2/tests/unit_tests/utils/test_report_execution.py
T
Elizabeth ThompsonandClaude 9bbf97b60d feat(reports): structured chart-holder diagnostics parity for capture logging
Enriches the report readiness/capture log lines with the per-state
holder breakdown proven out in production:

- ChartHolderDiagnostics (report_execution.py): structured counts
  separating capture readiness from semantic success, with
  TERMINAL_CHART_HOLDER_STATES and the explicit
  CHART_HOLDER_SEMANTIC_POLICY=deliver_terminal_errors_with_warning.
- report_readiness_poll/_ready/_terminal now carry rendered/empty/
  error/virtualized/unready counts and semantic_success on both the
  Playwright and Selenium paths.
- report_readiness_tile: per-tile readiness diagnostics during tiled
  capture, with each holder's strongest observed terminal state
  aggregated across tiles for the final ready line.
- report_semantic_status: a WARNING whenever capture readiness is
  satisfied but the artifact contains terminal chart errors — delivery
  is not semantic completeness, and this makes the distinction
  operator-visible (and alertable) per tile and per capture.
- Restores the explicit zero-holder thumbnail diagnostic on the
  no-context path.

Logging-only: no readiness predicate, budget, or delivery behavior
changes. Unit tests pin the diagnostics counts and the enriched line
formats.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 01:38:24 +00:00

253 lines
8.1 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 uuid import UUID
import pytest
from superset.utils.report_execution import (
ChartHolderDiagnostics,
get_report_task_timeout_options,
MIN_REPORT_EXECUTION_WORK_SECONDS,
ReportExecutionBudgetExceededError,
ReportExecutionContext,
ReportExecutionDeadline,
resolve_report_execution_budget_seconds,
validate_report_execution_config,
)
def _report_config(**overrides: int) -> dict[str, int | bool]:
config: dict[str, int | bool] = {
"ALERT_REPORTS_WORKING_TIME_OUT_KILL": True,
"ALERT_REPORTS_EXECUTION_BUDGET_SECONDS": 900,
"ALERT_REPORTS_EXECUTION_CAPTURE_RESERVE_SECONDS": 60,
"ALERT_REPORTS_EXECUTION_DELIVERY_RESERVE_SECONDS": 120,
"ALERT_REPORTS_EXECUTION_CLEANUP_RESERVE_SECONDS": 30,
"ALERT_REPORTS_EXECUTION_HARD_TIMEOUT_GRACE_SECONDS": 30,
"ALERT_REPORTS_WORKING_SOFT_TIME_OUT_LAG": 1,
"ALERT_REPORTS_WORKING_TIME_OUT_LAG": 10,
}
config.update(overrides)
return config
def test_chart_holder_diagnostics_separate_terminal_errors_from_success() -> None:
diagnostics = ChartHolderDiagnostics.from_holder_states(
[
{"chartId": "1", "state": "rendered"},
{"chartId": "2", "state": "rendered"},
{"chartId": "3", "state": "empty"},
{"chartId": "4", "state": "error"},
{"chartId": "5", "state": "virtualized"},
]
)
assert diagnostics.mounted_holders == 5
assert diagnostics.ready_holders == 5
assert diagnostics.rendered_holders == 2
assert diagnostics.empty_holders == 1
assert diagnostics.error_holders == 1
assert diagnostics.virtualized_holders == 1
assert diagnostics.unready_holders == 0
assert diagnostics.semantic_success is False
def test_chart_holder_diagnostics_count_unready_holders() -> None:
diagnostics = ChartHolderDiagnostics.from_holder_states(
[
{"chartId": "1", "state": "rendered"},
{"chartId": "2", "state": "waiting_on_database"},
{"chartId": "3", "state": "nothing_mounted"},
]
)
assert diagnostics.ready_holders == 1
assert diagnostics.unready_holders == 2
assert diagnostics.semantic_success is False
def test_chart_holder_diagnostics_do_not_treat_zero_holders_as_success() -> None:
diagnostics = ChartHolderDiagnostics.from_holder_states([])
assert diagnostics.mounted_holders == 0
assert diagnostics.semantic_success is False
def test_report_deadline_derives_phase_timeout_from_one_clock() -> None:
clock_value = 100.0
deadline = ReportExecutionDeadline(
total_seconds=900,
started_at=0,
_clock=lambda: clock_value,
)
context = ReportExecutionContext(
execution_id=UUID("084e7ee6-5557-4ecd-9632-b7f39c9ec524"),
report_schedule_id=7,
deadline=deadline,
capture_reserve_seconds=60,
delivery_reserve_seconds=120,
cleanup_reserve_seconds=30,
)
assert deadline.elapsed_seconds == 100
assert deadline.remaining_seconds == 800
assert context.readiness_reserve_seconds == 210
assert (
deadline.timeout_seconds(
"chart_readiness",
reserve_seconds=context.readiness_reserve_seconds,
)
== 590
)
assert (
deadline.timeout_seconds(
"screenshot_capture",
reserve_seconds=context.post_capture_reserve_seconds,
)
== 650
)
assert (
deadline.timeout_seconds(
"notification_delivery",
reserve_seconds=context.cleanup_reserve_seconds,
)
== 770
)
def test_report_deadline_exhaustion_names_phase() -> None:
deadline = ReportExecutionDeadline(
total_seconds=900,
started_at=0,
_clock=lambda: 700,
)
with pytest.raises(
ReportExecutionBudgetExceededError,
match="before chart_readiness",
):
deadline.timeout_seconds(
"chart_readiness",
reserve_seconds=210,
)
def test_report_context_rejects_reserves_that_consume_deadline() -> None:
deadline = ReportExecutionDeadline(total_seconds=210)
with pytest.raises(ValueError, match="must total less"):
ReportExecutionContext(
execution_id=UUID("084e7ee6-5557-4ecd-9632-b7f39c9ec524"),
report_schedule_id=7,
deadline=deadline,
capture_reserve_seconds=60,
delivery_reserve_seconds=120,
cleanup_reserve_seconds=30,
)
def test_report_deadline_rejects_nonpositive_budget() -> None:
with pytest.raises(ValueError, match="greater than zero"):
ReportExecutionDeadline(total_seconds=0)
def test_report_task_limits_align_soft_timeout_with_budget() -> None:
config = _report_config()
assert get_report_task_timeout_options(
is_report=True,
working_timeout=3600,
config=config,
) == {"soft_time_limit": 900, "time_limit": 930}
assert get_report_task_timeout_options(
is_report=False,
working_timeout=3600,
config=config,
) == {"soft_time_limit": 3601, "time_limit": 3610}
def test_working_timeout_caps_report_budget() -> None:
"""A per-schedule working_timeout below the global budget keeps its
historical user-facing meaning: it caps the effective budget and the
derived Celery limits."""
config = _report_config()
assert resolve_report_execution_budget_seconds(config, working_timeout=600) == 600.0
assert get_report_task_timeout_options(
is_report=True,
working_timeout=600,
config=config,
) == {"soft_time_limit": 600, "time_limit": 630}
def test_working_timeout_above_budget_does_not_raise_it() -> None:
config = _report_config()
assert (
resolve_report_execution_budget_seconds(config, working_timeout=7200) == 900.0
)
def test_missing_working_timeout_uses_global_budget() -> None:
config = _report_config()
assert (
resolve_report_execution_budget_seconds(config, working_timeout=None) == 900.0
)
def test_tiny_working_timeout_floors_at_minimum_viable_budget() -> None:
"""A working_timeout below the summed phase reserves cannot construct a
valid execution context; it is floored (with a warning) so the report
fails cleanly at its first phase check instead of erroring at setup."""
config = _report_config()
reserves_total = 60 + 120 + 30
budget = resolve_report_execution_budget_seconds(config, working_timeout=120)
assert budget == reserves_total + MIN_REPORT_EXECUTION_WORK_SECONDS
@pytest.mark.parametrize(
("overrides", "message"),
[
({"ALERT_REPORTS_EXECUTION_BUDGET_SECONDS": 0}, "greater than zero"),
(
{"ALERT_REPORTS_EXECUTION_CAPTURE_RESERVE_SECONDS": -1},
"cannot be negative",
),
(
{"ALERT_REPORTS_EXECUTION_DELIVERY_RESERVE_SECONDS": 810},
"must total less",
),
(
{"ALERT_REPORTS_EXECUTION_HARD_TIMEOUT_GRACE_SECONDS": -1},
"grace cannot be negative",
),
],
)
def test_report_execution_config_rejects_invalid_startup_values(
overrides: dict[str, int],
message: str,
) -> None:
with pytest.raises(ValueError, match=message):
validate_report_execution_config(_report_config(**overrides))
def test_report_execution_config_accepts_defaults() -> None:
validate_report_execution_config(_report_config())