Files
ddadmin 7c9eeedc40 feat(stand): make up наполняет стенд стартовым миром, опись сторожит его
Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому
же состоянию, а в git лежит то, чем это состояние проверяется.

Что
- Опись мира `data/world-inventory.json`: паспорт (зерно, версия
  генератора, хеш каталога) и по строке на каждый из восьми дней — дата,
  число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит
  `test_inventory.py` — тем же способом, что свежесть описания выгрузки.
- Разовая служба `world-init` вышла из-под профиля и играет в топик восемь
  дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait`
  считает успешно отработавшую службу упавшей.
- `scripts/wait-for-world.sh` — вторая половина `make up`: приём
  асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения
  заливки. Ограниченный цикл опроса, не пауза наугад.
- Девятая проверка `make check-clickhouse`: подневный счёт событий против
  описи, рамка по датам стартового мира, счёт через `FINAL`. При
  расхождении называет, где искать, — в событиях или в браке.
- Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал
  1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с
  датой. Раздел 9 спеки закрыт: открытых вопросов не осталось.
- Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром
  (решение владельца). Оба заведены в словарь CONTEXT.md.

Проверка
`make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий.
`make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с),
`make test` — 407 тестов за 71 с, `make lint`, `make typecheck`,
`make config-test` зелёные.

Что проверка умеет краснеть, снято двумя поломками: снос партиции
2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье —
«сломан разбор». Строки опыта убраны, день переигран, счёт вернулся.
Тест свежести проверен молчаливой правкой цены в каталоге: покраснел.

Ссылка: #42
2026-08-07 18:37:01 +03:00

73 lines
3.4 KiB
Python

"""Канон сериализатора: набор ключей, их порядок и форма на проводе.
Сторожится здесь то, на чём стоит сторона хранилища: строгий приём сверяет
набор ключей и разбирает даты как ISO. Разойдись сериализатор с этим — событие
уйдёт в брак целиком, а поймается это уже на стенде.
"""
import json
import pytest
from clickstream_generator import day as day_module
from clickstream_generator import schema, serialize
from clickstream_generator.seeds import CANONICAL_SEED
DAY = 2
@pytest.fixture(scope="module")
def events() -> list[dict[str, object]]:
"""События дня, разобранные обратно из канонических байтов."""
today = day_module.stream(CANONICAL_SEED, DAY)
return [json.loads(payload) for payload in serialize.events(today)]
def test_every_event_carries_every_column(events):
"""Все 47 ключей всегда и в порядке контракта — у любого события.
«Пусто» по контракту — пустое значение, а не отсутствие ключа: пропавший
ключ уводит событие в брак целиком (ADR 0005). Порядок ключей — часть
канона: от него зависят байты, а значит и хеши описи.
"""
names = [column.name for column in schema.COLUMNS]
for event in events:
assert list(event) == names
def test_empty_is_a_value_not_a_hole(events):
"""У просмотра страницы торговые колонки пусты, но они есть."""
pageview = next(event for event in events if event["EventType"] == "pageview")
assert pageview["purchaseID"] == []
assert pageview["productPrice"] == []
assert pageview["GoalsReached"] == []
assert pageview["ecommerce"] == ""
def test_dates_go_as_iso(events):
"""Даты читаются глазами: `2026-06-03` и `2026-06-03T12:34:56Z`.
Весь смысл слоя STG в том, что менти открывает колонку `raw` обычным
клиентом и разбирает событие сам; число эпохи этот урок убивает.
"""
for event in events[:100]:
assert event["EventDate"] == "2026-06-03"
assert event["UTCEventTime"].endswith("Z")
assert len(event["UTCEventTime"]) == len("2026-06-03T12:34:56Z")
def test_ecommerce_is_a_string_with_json_inside(events):
"""`ecommerce` уезжает строкой, как отдаёт Метрика, — материал лабы."""
purchase = next(event for event in events if event["EventType"] == "purchase")
assert isinstance(purchase["ecommerce"], str)
inside = json.loads(purchase["ecommerce"])
assert inside["purchase"]["actionField"]["id"] == purchase["purchaseID"][0]
def test_limit_takes_the_beginning_of_the_day(events):
"""Ограниченная пачка — начало дня, а не его пересборка другими байтами."""
today = day_module.stream(CANONICAL_SEED, DAY)
short = serialize.events(today, limit=10)
assert len(short) == 10
assert [json.loads(payload) for payload in short] == events[:10]