Files
clickstream-data-platform/generator/tests/test_seeds.py
T
ddadminandClaude Opus 5 2502f906a8 fix(generator): правки по двум линиям ревью плана состава
- Зачем:
  - ревью нашло два места, где код и документы говорили неправду, и
    несколько мест, где имена или комментарии вводили в заблуждение.
- Что:
  - обещание докстроки `seeds.py` подкреплено тестом: адрес в дереве
    даёт тот же подпоток, что цепочка `spawn`.
  - в тесте гарантии пар убран сторож-тавтология, вместо него проверка,
    что заказы назначены с двух разных кук.
  - `Cohort.visitors_on` — «кто пришёл в день D» спрашивается у когорты,
    а не собирается снаружи из четырёх её массивов.
  - имена: `CLIENT_ID_LIMIT`, `_RETURN_*_CUMULATIVE`, `first_of_day`,
    `WEEKLY_PROFILE_PERCENT` — профиль недели один на весь мир, по нему
    же пойдёт трафик дня-функции (#39).
  - спека: в дерево зёрен внесена ветвь предыстории; окно активности —
    от первого визита человека, общее на обе куки (иначе загляд назад
    ленивой формы удваивается); оценка накопленной аудитории больше не
    спорит с измерением.
  - CONTEXT.md: «подпоток» и «конфигурация мира».
- Проверка:
  - make test (296), make lint, make typecheck; мутации перепроверены
    после переноса среза дня в `Cohort`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:33:27 +03:00

83 lines
3.5 KiB
Python

"""Свойства иерархии зёрен, на которых держится детерминизм.
Проверяется не «числа такие-то» (они зависят от numpy и закреплены
`uv.lock`), а три обещания спеки, раздел 2: подпоток определяется позицией
в дереве, а не порядком вычислений; продление истории днём N+1 не трогает
дни 1…N; правка одного компонента не задевает соседние.
"""
import numpy as np
from clickstream_generator.seeds import (
CANONICAL_SEED,
Component,
cohort_stream,
day_stream,
)
def first_draws(stream: np.random.Generator) -> list[int]:
"""Отпечаток подпотока: первые броски целыми."""
return stream.integers(0, 2**32, 8).tolist()
def test_canonical_seed_is_a_repository_constant():
assert isinstance(CANONICAL_SEED, int)
def test_addressing_a_subtree_equals_spawning_down_to_it():
"""Свойство numpy, на котором стоит вся раздача зерна.
Потомок определяется парой (зерно, позиция в дереве): выписанный руками
`spawn_key` даёт тот же подпоток, что цепочка `spawn`. Иначе результат
зависел бы от порядка вычислений, и «параллельно равно последовательно»
не выполнялось бы.
"""
chained = np.random.SeedSequence(CANONICAL_SEED).spawn(1)[0].spawn(4)[3]
addressed = np.random.SeedSequence(CANONICAL_SEED, spawn_key=(0, 3))
assert chained.spawn_key == addressed.spawn_key
assert first_draws(np.random.Generator(np.random.PCG64(chained))) == first_draws(
np.random.Generator(np.random.PCG64(addressed))
)
def test_stream_is_a_position_in_the_tree_not_an_order_of_calls():
straight = first_draws(cohort_stream(CANONICAL_SEED, 5))
cohort_stream(CANONICAL_SEED, 0)
day_stream(CANONICAL_SEED, 3, Component.TRAFFIC)
detoured = first_draws(cohort_stream(CANONICAL_SEED, 5))
assert straight == detoured
def test_cohorts_of_different_days_are_independent():
draws = [first_draws(cohort_stream(CANONICAL_SEED, day)) for day in range(5)]
assert len({tuple(draw) for draw in draws}) == len(draws)
def test_prehistory_does_not_collide_with_the_axis():
"""Дни до D0 отрицательны, позиция в дереве — нет: своя ветвь."""
for depth in range(1, 5):
assert first_draws(cohort_stream(CANONICAL_SEED, -depth)) != first_draws(
cohort_stream(CANONICAL_SEED, depth)
)
def test_day_components_do_not_share_randomness():
draws = [
first_draws(day_stream(CANONICAL_SEED, 3, component)) for component in Component
]
assert len({tuple(draw) for draw in draws}) == len(Component)
def test_composition_and_day_are_separate_streams():
"""Состав мира ветвится сам по себе — день-функция его не сдвигает."""
assert first_draws(cohort_stream(CANONICAL_SEED, 3)) != first_draws(
day_stream(CANONICAL_SEED, 3, Component.TRAFFIC)
)
def test_another_seed_is_another_world():
assert first_draws(cohort_stream(CANONICAL_SEED, 3)) != first_draws(
cohort_stream(CANONICAL_SEED + 1, 3)
)