- Зачем:
- контракт вёл себя как хозяин чужого слоя: поле называлось dds_name, в
описании стоял столбец «Имя в DDS», а два теста прибивали имена
гвоздями. Спека же задала вид имени (snake_case), а не список: имена
атрибутов складывает модель данных DDS, и решать это не трекеру.
- Что:
- поле контракта и столбец описания стали нормализованным именем: имя
источника в нашем стиле. В описании и в докстринге сказано прямо, что
слой DDS называет атрибуты по своей модели.
- сняты оба теста на имена — копия имён DDS и конспект состава по
мастер-спеке. Они не проверяли верность имени, только неизменность, а
неизменность и так сторожит пересборка описания: молчаливой правки
контракта не бывает, она всплывает диффом документа.
- остались проверки формы: 47 колонок, уникальность, стили имён,
заполненность, согласие типов numpy и ClickHouse, порядок групп.
- спека генератора (раздел 3) и CONTEXT.md согласованы тем же
коммитом: уточнение внесено как расхождение, найденное при исполнении.
- Проверка:
- make test (248 тестов), make lint, make config-test;
- make docs, затем git diff --exit-code docs/ — пусто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""Проверки «описания выгрузки»: свежесть документа и полнота таблицы.
|
|
|
|
Документ собирается из контракта, значит расходиться они могут только одним
|
|
способом — контракт правили, документ не пересобрали. Ровно это здесь и
|
|
сторожится.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from clickstream_generator.schema import COLUMNS, Column, ColumnGroup
|
|
from clickstream_generator.schema_doc import render
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
DOC_PATH = REPO_ROOT / "docs" / "formats" / "clickstream-event.md"
|
|
|
|
# Номер и имя колонки из строки таблицы: по ним сверяется не только состав
|
|
# документа, но и его порядок — по нему сторона хранилища выпишет колонки.
|
|
TABLE_ROW = re.compile(r"^\| (\d+) \| `([^`]+)` \|", re.MULTILINE)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def rendered() -> str:
|
|
return render()
|
|
|
|
|
|
def test_doc_is_up_to_date(rendered: str):
|
|
assert DOC_PATH.exists(), f"описание выгрузки не найдено: {DOC_PATH}"
|
|
assert DOC_PATH.read_text(encoding="utf-8") == rendered, (
|
|
"описание выгрузки отстало от контракта — пересоберите: make docs"
|
|
)
|
|
|
|
|
|
def test_every_column_has_a_row(rendered: str):
|
|
assert len(TABLE_ROW.findall(rendered)) == len(COLUMNS)
|
|
|
|
|
|
def test_rows_follow_contract_order(rendered: str):
|
|
"""Строки идут в порядке контракта, а не просто нумеруются с 1 по 47."""
|
|
rows = [(int(number), name) for number, name in TABLE_ROW.findall(rendered)]
|
|
expected = [(number, column.name) for number, column in enumerate(COLUMNS, 1)]
|
|
assert rows == expected
|
|
|
|
|
|
@pytest.mark.parametrize("column", COLUMNS, ids=lambda column: column.name)
|
|
def test_column_is_described_in_full(column: Column, rendered: str):
|
|
"""Колонку описывает одна строка, и в ней всё, что несёт контракт."""
|
|
described = (
|
|
column.name,
|
|
column.clickhouse_type,
|
|
column.numpy_dtype,
|
|
column.normalized_name,
|
|
column.comment,
|
|
)
|
|
assert any(
|
|
all(value in line for value in described) for line in rendered.splitlines()
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("group", list(ColumnGroup), ids=lambda group: group.name)
|
|
def test_group_is_a_heading(group: ColumnGroup, rendered: str):
|
|
assert f"\n## {group.value}\n" in rendered
|