103 lines
6.1 KiB
Markdown
103 lines
6.1 KiB
Markdown
---
|
||
title: 🔁 Round-trip key verification — проверка нового ключа конвертера
|
||
namespace: personal
|
||
type: how-to
|
||
created: '2026-09-17'
|
||
updated: '2026-09-17'
|
||
tags:
|
||
- personal
|
||
- how-to
|
||
- testing
|
||
- converters
|
||
- reference
|
||
related:
|
||
- '[[personal/projects/zont-config-compiler]]'
|
||
---
|
||
|
||
# 🔁 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` («записалось») — **не** проверка. Подменить новое значение и пересобрать; эффект
|
||
обязан появиться в выходе.
|
||
|
||
```bash
|
||
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)
|
||
# ✅ изменилась РОВНО одна строка, в ней маркер
|
||
# ❌ ноль изменений = энкодер ключ проигнорировал → искать ДРУГУЮ ветку кода
|
||
```
|
||
|
||
Проверять **обе** половины:
|
||
1. **Парсер** — ключ есть в промежуточном файле, на ожидаемом месте.
|
||
2. **Энкодер** — подмена доезжает до выхода, `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. Новый ключ, где эмитится, какие точки входа поправлены.
|
||
2. Команду подмены и наблюдённый результат («1 строка изменилась, маркер на месте»).
|
||
3. Питфолл «две точки входа» — чтобы следующая сессия сразу проверяла обе.
|