- Зачем: - старый state и ручной live-генератор могли тихо писать события в новый мир данных. - Что: - добавлена общая проверка чистого стенда для Airflow и host-путей. - защищены backfill, import, continue и clean-start сценарии live-generator. - описан миграционный отказ старого state. - Проверка: - make generator-test; docker compose config --quiet; py_compile; bash -n.
137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
"""
|
||
Контракт DAG generator_control без запуска Airflow.
|
||
"""
|
||
|
||
import ast
|
||
from pathlib import Path
|
||
|
||
from clickstream_generator.launch import PROFILES
|
||
|
||
|
||
DAG_PATH = Path(__file__).parents[2] / "airflow" / "dags" / "generator_control_dag.py"
|
||
REPO_ROOT = Path(__file__).parents[2]
|
||
|
||
|
||
def _tree():
|
||
return ast.parse(DAG_PATH.read_text(encoding="utf-8"))
|
||
|
||
|
||
def test_dag_file_exists_and_uses_dynamic_profiles():
|
||
"""DAG берёт варианты профилей из PROFILES, а не из ручного списка."""
|
||
text = DAG_PATH.read_text(encoding="utf-8")
|
||
|
||
assert "dag_id=\"generator_control\"" in text
|
||
assert "sorted(PROFILES)" in text
|
||
for profile in PROFILES:
|
||
assert profile not in {"hardcoded-profile"}
|
||
|
||
|
||
def test_trigger_form_has_expected_param_enums():
|
||
"""Форма запуска ограничивает операции и профили."""
|
||
text = DAG_PATH.read_text(encoding="utf-8")
|
||
|
||
assert 'enum=["backfill", "import", "check"]' in text
|
||
assert "enum=sorted(PROFILES)" in text
|
||
assert '"duration": Param(' in text
|
||
assert '"artifact_path": Param(' in text
|
||
|
||
|
||
def test_dag_branches_and_waits_for_etl_completion():
|
||
"""Backfill/import запускают ETL и ждут его завершения перед check."""
|
||
text = DAG_PATH.read_text(encoding="utf-8")
|
||
|
||
assert "BranchPythonOperator" in text
|
||
assert "TriggerDagRunOperator" in text
|
||
assert 'trigger_dag_id="etl_pipeline"' in text
|
||
assert "wait_for_completion=True" in text
|
||
assert 'allowed_states=["success"]' in text
|
||
assert 'failed_states=["failed"]' in text
|
||
|
||
|
||
def test_no_docker_or_continue_operation_in_dag():
|
||
"""Пульт не управляет Docker и не содержит операцию continue."""
|
||
text = DAG_PATH.read_text(encoding="utf-8")
|
||
|
||
assert "docker" not in text.lower()
|
||
assert '"continue"' not in text
|
||
|
||
|
||
def test_compose_mounts_generator_code_without_socket_and_gates_live_service():
|
||
"""Airflow видит код генератора, но не получает Docker socket."""
|
||
text = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||
|
||
assert "PYTHONPATH: /opt/airflow/generator_src" in text
|
||
assert "./generator/src:/opt/airflow/generator_src:ro" in text
|
||
assert "/var/run/docker.sock" not in text
|
||
assert "profiles:\n - live-generator" in text
|
||
|
||
|
||
def test_down_and_clean_include_live_generator_profile():
|
||
"""Остановка стенда удаляет профильный live-генератор."""
|
||
text = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||
|
||
assert "down:\n\t$(COMPOSE) --profile live-generator down" in text
|
||
assert "clean:\n\t$(COMPOSE) --profile live-generator down -v --remove-orphans" in text
|
||
|
||
|
||
def test_console_generator_paths_precheck_clean_stand_before_writes():
|
||
"""Backfill/import с хоста проверяют чистый стенд до записи."""
|
||
run_generator = (REPO_ROOT / "scripts" / "run_generator.sh").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
import_artifact = (
|
||
REPO_ROOT / "scripts" / "import_startup_history_artifact.sh"
|
||
).read_text(encoding="utf-8")
|
||
|
||
assert "bash \"${SCRIPT_DIR}/assert_stand_clean.sh\" clean" in run_generator
|
||
assert run_generator.index("assert_stand_clean.sh") < run_generator.index(
|
||
"run --rm --no-deps"
|
||
)
|
||
assert "bash \"${SCRIPT_DIR}/assert_stand_clean.sh\" clean" in import_artifact
|
||
assert import_artifact.index("assert_stand_clean.sh") < import_artifact.index(
|
||
"startup_history_artifact import"
|
||
)
|
||
|
||
|
||
def test_console_continue_checks_state_or_clean_stand_before_live_start():
|
||
"""Continue не стартует новый live-мир на грязном стенде без state."""
|
||
run_generator = (REPO_ROOT / "scripts" / "run_generator.sh").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
assert "elif [[ \"${VERB}\" == \"continue\" ]]" in run_generator
|
||
assert "bash \"${SCRIPT_DIR}/assert_stand_clean.sh\" continue" in run_generator
|
||
assert run_generator.index("assert_stand_clean.sh\" continue") < run_generator.index(
|
||
"up -d --build generator"
|
||
)
|
||
|
||
|
||
def test_clean_start_paths_include_live_generator_profile():
|
||
"""Все чистые сбросы видят профильный live-генератор."""
|
||
generated_history = (
|
||
REPO_ROOT / "scripts" / "run_generated_history_analytics.sh"
|
||
).read_text(encoding="utf-8")
|
||
|
||
assert "--profile live-generator down -v --remove-orphans" in generated_history
|
||
|
||
|
||
def test_host_clean_precheck_checks_live_before_kafka_and_stg():
|
||
"""Host backfill/import сначала проверяет, что live-генератор не запущен."""
|
||
text = (REPO_ROOT / "scripts" / "assert_stand_clean.sh").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
assert "--mode live" in text
|
||
assert text.index("--mode live") < text.index("clickhouse-client")
|
||
assert text.index("clickhouse-client") < text.index("--mode \"${CHECK_MODE}\"")
|
||
|
||
|
||
def test_airflow_and_generator_kafka_dependency_versions_match():
|
||
"""Airflow и генератор используют одну версию kafka-python."""
|
||
airflow_req = (REPO_ROOT / "airflow" / "requirements.txt").read_text(encoding="utf-8")
|
||
generator_req = (REPO_ROOT / "generator" / "requirements.txt").read_text(encoding="utf-8")
|
||
|
||
assert "kafka-python==2.0.6" in airflow_req
|
||
assert "kafka-python==2.0.6" in generator_req
|
||
assert "prometheus-client==0.21.1" in airflow_req
|