6.1 KiB
title, namespace, type, created, updated, tags, related
| title | namespace | type | created | updated | tags | related | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 🔁 Round-trip key verification — проверка нового ключа конвертера | personal | how-to | 2026-09-17 | 2026-09-17 |
|
|
🔁 Round-trip key verification
Зеркало навыка
roundtrip-key-verification(категорияsoftware-development, Hermes). Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит87e315c). Разбор в проекте — personal/projects/zont-config-compiler §10.1.
Когда применять
Любой двусторонний конвертер с тестом байтовой точности: .txt ⇄ .yml, сериализатор с
decode + encode, dump/undump AST, DB ⇄ ORM.
Триггер: добавил новый ключ в декодированный вывод и round-trip зелёный.
🔴 Ловушка
Байтовый round-trip доказывает, что вывод парсера == вход. Про энкодер он не говорит ничего — энкодер получает ровно то, что парсер положил.
Если энкодер игнорирует новый ключ, тест всё равно зелёный:
original.txt → parse → YAML(с новым ключом) → encode(ключ игнорируется) → original.txt ✅ diff = 0
▲ ключ добавлен ▲ ключ потерян, старое поле доносит байты
Потеря невидима: старое поле (args) рядом и по-прежнему собирает байты.
Реальный случай: ключ set_var добавлен в парсер типа 59, энкодер поправлен только
в emit_action. Вторая точка входа — инлайн-ветка emit_step ('descr' in step and 'args' in step) —
ключ игнорировала. Round-trip 🟢, правка пользователя молча терялась.
Обязательный шаг: тест подменой (mutation)
read back («записалось») — не проверка. Подменить новое значение и пересобрать; эффект
обязан появиться в выходе.
cd /tmp && rm -rf t && mkdir t && cd t
cp <реальный-артефакт>.yml t.yml
python3 - <<'EOF'
s = open('t.yml', encoding='utf-8').read()
old = "...блок со старым значением нового ключа..."
new = "...тот же блок, значение заменено на маркер (напр. 7777)..."
assert s.count(old) == 1, s.count(old) # assert уникальности — без случайных мультиправок
open('t.yml', 'w', encoding='utf-8').write(s.replace(old, new))
EOF
python3 <проект>/encode.py t.yml > out.txt
diff <(iconv -f <вх> -t utf-8 <оригинал> | tr -d '\r' | sort) \
<(iconv -f <вых> -t utf-8 out.txt | tr -d '\r' | sort)
# ✅ изменилась РОВНО одна строка, в ней маркер
# ❌ ноль изменений = энкодер ключ проигнорировал → искать ДРУГУЮ ветку кода
Проверять обе половины:
- Парсер — ключ есть в промежуточном файле, на ожидаемом месте.
- Энкодер — подмена доезжает до выхода,
diff= ровно 1 строка.
Найти ВСЕ точки входа
У конвертеров их обычно две и больше: рекурсивный обход и инлайн/ссылочная ветка, либо проход по реестру типов и явный цикл.
grep -n "def emit_<x>\|def dump_<x>" *.py— выписать всех вызывающих.- Не править одну и полагать, что покрыта другая. Дублированные ветки — норма.
- Симптом пропущенной ветки: в тесте подменой остаётся старое значение.
В каждой ветке ставить проверку типа, чтобы плохое значение падало громко (exit 2),
а не тихо уходило сырым полем.
Смежные питфоллы
- Проверять тип производного ключа. Парсер, сохраняющий whole-float как float (
14.5,1.0), даст не-int для ключа-идентификатора. Guard:isinstance(v, int) and not isinstance(v, bool) and not isinstance(v, float) and v != 0.0= «цели нет», а не валидный id — пропускать, не выдумывать. - Один объект в двух секциях → ключ править во всех местах отображения, иначе одна проекция показывает новую семантику, другая старую. Round-trip и тут зелёный.
- Считать вхождения по данным, не по плану: «12 элементов» из плана оказались 10 в конфиге.
- Не резолвить имя из другого объекта. Производный ключ несёт сырой id;
target_nameвместоtarget— запрещённая подстановка.
Что записать после проверки
- Новый ключ, где эмитится, какие точки входа поправлены.
- Команду подмены и наблюдённый результат («1 строка изменилась, маркер на месте»).
- Питфолл «две точки входа» — чтобы следующая сессия сразу проверяла обе.