api/tests/synthetic: детерминированный генератор s01…s13 (openpyxl, метки времени книги и zip зафиксированы), минимальный писатель BIFF12 для s13.xlsb (ruling R-9, читается pyxlsb), тесты сценариев спеки osv-parsing на файлах из fixtures/osv/synthetic/ (таблица «файл → ожидания», s13 равен s02 кроме предупреждения xlsb), тест побайтовой воспроизводимости генерации. synthetic_files() учитывает только файлы Excel (README.md каталога не файл набора). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
73 lines
3.2 KiB
Python
73 lines
3.2 KiB
Python
"""Генератор синтетики: детерминированность, состав набора и читаемость xlsb-писателя (не требует каталога fixtures)."""
|
||
|
||
import re
|
||
import time
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
from src.parser.readers import read_book
|
||
from tests.synthetic.generate import FILE_NAMES, generate_all
|
||
from tests.synthetic.xlsb_writer import write_xlsb
|
||
|
||
# Сколько ждать между двумя генерациями: у записей zip разрешение метки времени — 2 с
|
||
ZIP_TIME_RESOLUTION_SECONDS = 2.1
|
||
LONG_TEXT_LENGTH = 400
|
||
BIG_ROW = 70000
|
||
|
||
|
||
def test_generation_is_reproducible_byte_for_byte(tmp_path: Path) -> None:
|
||
"""Повторный запуск (позже, чем разрешение меток времени) даёт те же байты для каждого файла набора."""
|
||
first = generate_all(tmp_path / "first")
|
||
time.sleep(ZIP_TIME_RESOLUTION_SECONDS)
|
||
second = generate_all(tmp_path / "second")
|
||
|
||
assert [path.name for path in first] == [path.name for path in second]
|
||
for left, right in zip(first, second, strict=True):
|
||
assert left.read_bytes() == right.read_bytes(), left.name
|
||
|
||
|
||
def test_generated_set_is_s01_to_s13_with_one_xlsb(tmp_path: Path) -> None:
|
||
names = [path.name for path in generate_all(tmp_path)]
|
||
|
||
assert names == list(FILE_NAMES)
|
||
assert [re.match(r"s(\d\d)_", name) is not None for name in names] == [True] * len(names)
|
||
assert [name[1:3] for name in names] == [f"{number:02d}" for number in range(1, len(names) + 1)]
|
||
assert [name for name in names if name.endswith(".xlsb")] == ["s13_no_outline_indent.xlsb"]
|
||
assert sorted(path.name for path in tmp_path.iterdir()) == sorted(names)
|
||
|
||
|
||
def test_xlsb_writer_round_trips_through_the_reader() -> None:
|
||
"""Тексты (с ведущими пробелами и кириллицей), числа и несколько листов читаются `src.parser.readers`."""
|
||
long_text = "щ" * LONG_TEXT_LENGTH # запись длиннее 127 байт: длина кодируется двумя байтами
|
||
data = write_xlsb(
|
||
{
|
||
"Лист_1": {
|
||
(1, 1): "Привет",
|
||
(2, 3): Decimal("1234567.89"),
|
||
(3, 1): " отступ",
|
||
(4, 2): long_text,
|
||
(BIG_ROW, 4): 7,
|
||
},
|
||
"Второй": {(1, 1): "Привет"},
|
||
}
|
||
)
|
||
|
||
sheets, warnings = read_book(data, "XLSB")
|
||
|
||
assert [sheet.name for sheet in sheets] == ["Лист_1", "Второй"]
|
||
first = sheets[0]
|
||
assert first.text_at(1, 1) == "Привет"
|
||
assert first.cell(2, 3).value == Decimal("1234567.89")
|
||
assert (first.text_at(3, 1), first.cell(3, 1).indent) == ("отступ", 4)
|
||
assert first.text_at(4, 2) == long_text
|
||
assert first.cell(BIG_ROW, 4).value == Decimal(7)
|
||
assert first.n_rows == BIG_ROW
|
||
assert sheets[1].text_at(1, 1) == "Привет"
|
||
assert warnings == ["xlsb: outline level недоступен"]
|
||
|
||
|
||
def test_xlsb_writer_is_deterministic() -> None:
|
||
cells = {"Лист_1": {(1, 1): "а", (2, 2): Decimal("1.50")}}
|
||
|
||
assert write_xlsb(cells) == write_xlsb(cells)
|