test(generator): усилены проверки startup-history и Superset
- Зачем: - зелёный результат проверок должен означать фактический стык и точный контракт дашборда. - Что: - проверка startup-history читает manifest, требует live seam и непустой ODS. - добавлен быстрый runtime gate для daily-wave и live-продолжения. - Superset sync ограничен целевым dashboard и сверяет существенные params. - Проверка: - docker target tests; make generator-test; make generated-history-runtime-check.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_generated_history_check_uses_actual_manifest_boundary_and_profile():
|
||||
"""Проверка берёт стык и профиль из manifest фактического прогона."""
|
||||
script = (REPO_ROOT / "scripts" / "check_generated_analytics.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||||
|
||||
assert "kafka-manifest-summary" in script
|
||||
assert "manifest_model_t_end" in script
|
||||
assert "CH_MODEL_T_END=\"$(clickhouse_datetime_literal \"${manifest_model_t_end}\")\"" in script
|
||||
assert "GEN_LAUNCH_PROFILE" in makefile
|
||||
assert "PROFILE" in makefile
|
||||
|
||||
|
||||
def test_generated_history_check_does_not_skip_required_live_seam():
|
||||
"""Обычная проверка требует реальные live-строки после стыка."""
|
||||
script = (REPO_ROOT / "scripts" / "check_generated_analytics.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||||
|
||||
assert 'CHECK_LIVE_SEAM="${CHECK_LIVE_SEAM:-1}"' in script
|
||||
assert 'if [[ "${should_check_live_seam}" == "1" ]] && (( live_rows == 0 )); then' in script
|
||||
assert "live-продолжение не записало строки" in script
|
||||
assert 'CHECK_LIVE_SEAM="$${CHECK_LIVE_SEAM:-1}"' in makefile
|
||||
|
||||
|
||||
def test_clean_generated_history_run_explicitly_skips_live_seam():
|
||||
"""Чистый backfill без live-продолжения отключает проверку стыка явно."""
|
||||
script = (REPO_ROOT / "scripts" / "run_generated_history_analytics.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "CHECK_LIVE_SEAM=0" in script
|
||||
assert script.index("CHECK_LIVE_SEAM=0") < script.index('bash "${SCRIPT_DIR}/check_generated_analytics.sh"')
|
||||
|
||||
|
||||
def test_runtime_gate_has_bounded_daily_wave_live_seam_path():
|
||||
"""Runtime gate issue 17 не использует полный daily-wave на 2 суток."""
|
||||
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||||
script = (REPO_ROOT / "scripts" / "run_generated_history_runtime_check.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "generated-history-runtime-check" in makefile
|
||||
assert 'PROFILE="${PROFILE:-daily-wave}"' in script
|
||||
assert 'GEN_HISTORY_DURATION="${GEN_HISTORY_DURATION:-1h}"' in script
|
||||
assert 'LIVE_SECONDS="${LIVE_SECONDS:-25}"' in script
|
||||
assert "stg_rows_after_live > stg_rows_before_live" in script
|
||||
assert "GEN_RUN_MODE=live" in script
|
||||
assert "GEN_STATE_RESET=false" in script
|
||||
assert "up -d --build generator" in script
|
||||
assert "CHECK_LIVE_SEAM=1" in script
|
||||
assert "REQUIRE_SUPERSET=0" in script
|
||||
|
||||
|
||||
def test_generated_history_check_rejects_empty_ods_context():
|
||||
"""ODS-блок стыка падает, если в ODS нет строк для переходящих визитов."""
|
||||
script = (REPO_ROOT / "scripts" / "check_generated_analytics.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "ods_device_rows" in script
|
||||
assert "ods_geo_rows" in script
|
||||
assert "ODS device пустой на стыке" in script
|
||||
assert "ODS geo пустой на стыке" in script
|
||||
@@ -28,10 +28,28 @@ def load_exported_dashboard():
|
||||
return json.loads(export_path.read_text())
|
||||
|
||||
|
||||
def expected_export_params(chart, actual_params):
|
||||
return {
|
||||
**chart["params"],
|
||||
"datasource": actual_params["datasource"],
|
||||
"viz_type": chart["viz_type"],
|
||||
}
|
||||
|
||||
|
||||
class FakeDashboard:
|
||||
def __init__(self, slug):
|
||||
self.slug = slug
|
||||
|
||||
|
||||
class FakeChart:
|
||||
def __init__(self, chart_id, slice_name):
|
||||
def __init__(self, chart_id, slice_name, datasource_id=1, dashboard_slugs=None):
|
||||
self.id = chart_id
|
||||
self.slice_name = slice_name
|
||||
self.datasource_id = datasource_id
|
||||
self.dashboards = [
|
||||
FakeDashboard(slug)
|
||||
for slug in (dashboard_slugs or ["ecommerce-analytics"])
|
||||
]
|
||||
|
||||
|
||||
def test_geo_chart_uses_readable_top_countries_bar_config():
|
||||
@@ -62,15 +80,20 @@ def test_geo_chart_uses_readable_top_countries_bar_config():
|
||||
|
||||
|
||||
def test_exported_dashboard_uses_same_readable_geo_chart():
|
||||
module = load_dashboard_module()
|
||||
exported_dashboard = load_exported_dashboard()
|
||||
|
||||
expected_chart = chart_config(module, "🌍 Top Countries by Events")
|
||||
geo_chart = exported_chart(exported_dashboard, "🌍 Top Countries by Events")
|
||||
|
||||
assert geo_chart["viz_type"] == "echarts_timeseries_bar"
|
||||
params = json.loads(geo_chart["params"])
|
||||
assert params == expected_export_params(expected_chart, params)
|
||||
assert params["viz_type"] == "echarts_timeseries_bar"
|
||||
assert params["x_axis"] == "geo_country"
|
||||
assert params["metrics"][0]["label"] == "Events, pcs"
|
||||
assert params["x_axis_sort"] == "Events, pcs"
|
||||
assert params["x_axis_sort_asc"] is False
|
||||
assert params["show_legend"] is True
|
||||
assert params["rich_tooltip"] is True
|
||||
assert params["y_axis_title"] == "Events, pcs"
|
||||
@@ -79,6 +102,24 @@ def test_exported_dashboard_uses_same_readable_geo_chart():
|
||||
assert position_json["CHART-7"]["meta"]["sliceName"] == "🌍 Top Countries by Events"
|
||||
|
||||
|
||||
def test_exported_dashboard_chart_params_match_current_config():
|
||||
module = load_dashboard_module()
|
||||
exported_dashboard = load_exported_dashboard()
|
||||
|
||||
expected_charts = {
|
||||
chart["slice_name"]: chart
|
||||
for chart in module.CHARTS_CONFIG
|
||||
}
|
||||
|
||||
for exported in exported_dashboard["charts"]:
|
||||
actual_chart = exported["__Slice__"]
|
||||
actual_params = json.loads(actual_chart["params"])
|
||||
expected_chart = expected_charts[actual_chart["slice_name"]]
|
||||
|
||||
assert actual_chart["viz_type"] == expected_chart["viz_type"]
|
||||
assert actual_params == expected_export_params(expected_chart, actual_params)
|
||||
|
||||
|
||||
def test_exported_dashboard_chart_names_match_current_config():
|
||||
module = load_dashboard_module()
|
||||
exported_dashboard = load_exported_dashboard()
|
||||
@@ -118,13 +159,59 @@ def test_exported_dashboard_layout_matches_current_rows():
|
||||
|
||||
def test_choose_chart_to_sync_prefers_current_name_and_marks_old_name_duplicate():
|
||||
module = load_dashboard_module()
|
||||
old_chart = FakeChart(7, "🌍 Geography Map")
|
||||
current_chart = FakeChart(11, "🌍 Top Countries by Events")
|
||||
old_chart = FakeChart(7, "🌍 Geography Map", datasource_id=42)
|
||||
current_chart = FakeChart(11, "🌍 Top Countries by Events", datasource_id=42)
|
||||
|
||||
selected, duplicates = module.choose_chart_to_sync(
|
||||
[old_chart, current_chart],
|
||||
"🌍 Top Countries by Events",
|
||||
dataset_id=42,
|
||||
)
|
||||
|
||||
assert selected is current_chart
|
||||
assert duplicates == [old_chart]
|
||||
|
||||
|
||||
def test_choose_chart_to_sync_ignores_same_name_from_other_dataset():
|
||||
module = load_dashboard_module()
|
||||
other_dataset_chart = FakeChart(7, "🌍 Top Countries by Events", datasource_id=7)
|
||||
target_dataset_chart = FakeChart(11, "🌍 Geography Map", datasource_id=42)
|
||||
|
||||
selected, duplicates = module.choose_chart_to_sync(
|
||||
[other_dataset_chart, target_dataset_chart],
|
||||
"🌍 Top Countries by Events",
|
||||
dataset_id=42,
|
||||
)
|
||||
|
||||
assert selected is target_dataset_chart
|
||||
assert duplicates == []
|
||||
|
||||
|
||||
def test_dashboard_sync_ignores_same_name_same_dataset_from_other_dashboard():
|
||||
module = load_dashboard_module()
|
||||
other_dashboard_chart = FakeChart(
|
||||
7,
|
||||
"🌍 Top Countries by Events",
|
||||
datasource_id=42,
|
||||
dashboard_slugs=["mentee-dashboard"],
|
||||
)
|
||||
target_dashboard_chart = FakeChart(
|
||||
11,
|
||||
"🌍 Geography Map",
|
||||
datasource_id=42,
|
||||
dashboard_slugs=["ecommerce-analytics"],
|
||||
)
|
||||
|
||||
owned_charts = module.dashboard_owned_charts(
|
||||
[other_dashboard_chart, target_dashboard_chart],
|
||||
dashboard_slug="ecommerce-analytics",
|
||||
)
|
||||
selected, duplicates = module.choose_chart_to_sync(
|
||||
owned_charts,
|
||||
"🌍 Top Countries by Events",
|
||||
dataset_id=42,
|
||||
)
|
||||
|
||||
assert owned_charts == [target_dashboard_chart]
|
||||
assert selected is target_dashboard_chart
|
||||
assert duplicates == []
|
||||
|
||||
Reference in New Issue
Block a user