"""Сборка набора (спека §7): структура каталога, детерминированность, README прогона, граничные настройки.""" import hashlib import time from dataclasses import replace from pathlib import Path import pytest from synthgen import generate as gen from synthgen.__main__ import main from synthgen.config import Config, load_config SMALL = Config(group_size=3, periods=2, osv_counterparties_mean=5, no_inn_companies=1) def _digest(root: Path) -> dict[str, str]: return {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(root.rglob("*")) if p.is_file()} @pytest.fixture(scope="module") def default_run(tmp_path_factory): out = tmp_path_factory.mktemp("default") manifest = gen.generate(Config(), out) return out, manifest def test_layout(default_run): out, manifest = default_run assert (out / "README.md").stat().st_size > 0 assert (out / "config.yaml").is_file() assert (out / "directories" / "ecp-SYN.xlsx").is_file() codes = [f"C{i:02d}" for i in range(1, 6)] assert sorted(p.name for p in (out / "osv").iterdir()) == codes assert sorted(p.name for p in (out / "statements").iterdir()) == codes assert len(list(out.glob("osv/*/*/*.xlsx"))) == 5 * 4 * 9 assert len(list(out.glob("statements/*/*/ББ_*.xlsx"))) == 5 * 4 assert len(list(out.glob("statements/*/*/ОФР_*.xlsx"))) == 5 * 4 assert sorted(p.name for p in (out / "osv" / "C01").iterdir()) == ["2025-FY", "2025-Q1", "2025-Q2", "2025-Q3"] # манифест перечисляет все файлы набора on_disk = {p for p in out.rglob("*") if p.is_file()} assert set(manifest.files) == on_disk def test_determinism(tmp_path): a, b, c = tmp_path / "a", tmp_path / "b", tmp_path / "c" gen.generate(SMALL, a) gen.generate(SMALL, b) gen.generate(replace(SMALL, seed=2), c) assert _digest(a) == _digest(b) assert str(a) not in (a / "config.yaml").read_text(encoding="utf-8") d = tmp_path / "d" gen.generate(replace(SMALL, out=d), d) assert _digest(a) == _digest(d) # путь результата не попадает в файлы assert _digest(a) != _digest(c) def test_out_not_empty(tmp_path, capsys): out = tmp_path / "res" gen.generate(SMALL, out) with pytest.raises(FileExistsError): gen.generate(SMALL, out) junk = out / "old.txt" junk.write_text("старое", encoding="utf-8") gen.generate(SMALL, out, force=True) assert not junk.exists() assert (out / "README.md").is_file() args = ["--set", "group_size=3", "--set", "periods=1", "--set", "osv_counterparties_mean=3", "--out", str(out)] assert main(args) == 3 assert "не пуст" in capsys.readouterr().err assert main([*args, "--force"]) == 0 assert str(out) in capsys.readouterr().out def test_default_out_is_prefix_dir(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) assert main(["--set", "group_size=2", "--set", "periods=1", "--set", "osv_counterparties_mean=2", "--set", "prefix=ZZ"]) == 0 assert (tmp_path / "out" / "ZZ" / "README.md").is_file() @pytest.mark.parametrize( "overrides", [ {"group_size": 2}, {"group_size": 40, "periods": 1, "osv_counterparties_mean": 10}, {"no_inn_companies": 5}, {"vgo_mismatch_share": 1}, {"start": "2025-Q4", "periods": 2}, ], ids=["size2", "size40", "all-no-inn", "all-mismatch", "from-q4"], ) def test_edge_configs(tmp_path, overrides): base = {"osv_counterparties_mean": 4, "periods": 1, **overrides} cfg = load_config(None, [f"{k}={v}" for k, v in base.items()]) manifest = gen.generate(cfg, tmp_path / "x") readme = (tmp_path / "x" / "README.md").read_text(encoding="utf-8") assert readme.strip() if not manifest.pairs: assert "Пар ВГО нет" in readme def _dec(v): from decimal import Decimal return Decimal(str(v)) def test_readme_lists_mismatches(tmp_path): cfg = replace(SMALL, group_size=5, vgo_mismatch_share=_dec(1)) manifest = gen.generate(cfg, tmp_path / "x") readme = (tmp_path / "x" / "README.md").read_text(encoding="utf-8") mismatches = [(p, m) for p in manifest.pairs for m in p.mismatches] assert mismatches for p, m in mismatches: line = next((ln for ln in readme.splitlines() if p.a in ln and "→" in ln and m.period_key in ln and f"{m.pct}" in ln), None) assert line is not None, (p.a, p.b, m) assert "выше порога" in line or "ниже порога" in line def test_readme_notes(default_run): out, _ = default_run readme = (out / "README.md").read_text(encoding="utf-8") for needle in ("90.01", "62", "76", "80", "58", "За 3 месяца", "вариантн"): assert needle in readme assert "не оракул" in readme @pytest.mark.slow def test_large_run_time(tmp_path): cfg = Config(group_size=15, periods=6, osv_counterparties_mean=150) t0 = time.monotonic() manifest = gen.generate(cfg, tmp_path / "big") elapsed = time.monotonic() - t0 print(f"large run: {elapsed:.1f} s, files={len(manifest.files)}") assert elapsed < 300