mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
test: приёмка переписывает только объявленные эталоны
`passed` означает «в пределах порога», а не «байт в байт»: comparePng считает diffRatio, и статус ставится по нему. А приёмка копировала кандидата поверх КАЖДОГО эталона матрицы, поэтому подпороговый дрейф уезжал в контракт молча — и накапливался: каждая приёмка подтягивала эталон к последней среде, порог не пересекался никогда, а эталон уходил. Так1e341c60заменил 22 картинки, объявив четыре. Проект уже сталкивался с этим:ad3f9981восстанавливал девять уехавших эталонов руками. Такую работу обязан делать инструмент. Теперь копируются только сцены из --expect-change и --expect-new; остальные сохраняют и файл, и свой хеш из прежнего индекса. Индекс по-прежнему перезаписывается на полный набор — сирота или пропавшая запись делают манифест недействительным целиком. Решение вынесено в чистую функцию goldenAcceptancePlan: оно одно, и ошибка в нём дорога. Отсутствие прежнего хеша у необъявленной сцены — ошибка, а не повод взять кандидата: без эталона бывает только новая сцена, а она обязана быть названа в --expect-new. Логика вернулась в demo/golden/accept.mjs, где ей и место: после #344 эти файлы исключены из корпуса отпечатка, так что правка больше не требует пересборки и пересъёмки. scripts/golden-accept.mjs остался проходным вызовом ради документированной команды. Проверено сквозным прогоном на синтетическом кандидате: у двух сцен байты другие, объявлена одна — на диске изменились ровно два файла, эталон и индекс, а хеш второй сцены остался прежним. Два мутанта убиты руками: «брать кандидата вместо прежнего хеша» и «заменять всё». Issue: #351 User-Visible: no
This commit is contained in:
+10
-55
@@ -1,76 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Приёмка эталонов с объявлением намерения (#334).
|
||||
* Проходной вызов `demo/golden/accept.mjs` (#334, #350, #351).
|
||||
*
|
||||
* node scripts/golden-accept.mjs --reviewed --expect-change=<id,id>
|
||||
* node scripts/golden-accept.mjs --reviewed --expect-new=<id,id>
|
||||
* node scripts/golden-accept.mjs --reviewed --expect-change=<id> --from=<распакованный артефакт>
|
||||
*
|
||||
* Обёртка появилась потому, что до #344 любой `.mjs` из `demo/golden` входил в
|
||||
* корпус отпечатка, и правка инструмента приёмки объявляла устаревшими бандл и
|
||||
* оба манифеста. После #344 это ограничение снято, правило живёт в самом
|
||||
* `accept.mjs`, а этот файл остался ради документированной команды и передаёт
|
||||
* аргументы как есть.
|
||||
*
|
||||
* Два флага утверждают разное: `--expect-change` — «я знаю, почему старый кадр
|
||||
* изменился», `--expect-new` — «я посмотрел на новый кадр». Путаница между ними
|
||||
* останавливает приёмку (#350).
|
||||
*
|
||||
* Обёртка над `demo/golden/accept.mjs`, а не правка его самого: любой `.mjs` из
|
||||
* `demo/golden` входит в `sourceFingerprint`, поэтому его правка объявляет
|
||||
* устаревшими бандл и оба манифеста — см. `scripts/golden-acceptance.mjs`.
|
||||
*
|
||||
* Проверка идёт ДО делегирования: `accept.mjs` копирует картинки целым набором,
|
||||
* и запрет обязан сработать раньше, чем каталог эталонов будет тронут.
|
||||
* изменился», `--expect-new` — «я посмотрел на новый кадр». Необъявленная сцена
|
||||
* не переписывается вовсе: её эталон и хеш остаются прежними (#351).
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { goldenAcceptanceRefusal, goldenSilentDeclarations } from './golden-acceptance.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const argv = process.argv.slice(2);
|
||||
const value = (name) => {
|
||||
const found = argv.find((item) => item.startsWith(`--${name}=`));
|
||||
return found ? found.slice(name.length + 3) : '';
|
||||
};
|
||||
|
||||
if (!argv.includes('--reviewed')) {
|
||||
console.error('приёмка требует явного --reviewed');
|
||||
process.exit(2);
|
||||
}
|
||||
const from = resolve(value('from') || resolve(ROOT, 'artifacts/golden'));
|
||||
const list = (name) => value(name).split(',').map((id) => id.trim()).filter(Boolean);
|
||||
const declared = list('expect-change');
|
||||
const declaredNew = list('expect-new');
|
||||
|
||||
const reportPath = resolve(from, 'golden-report.json');
|
||||
if (!existsSync(reportPath)) {
|
||||
console.error(`отчёт кандидатов не найден: ${reportPath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
|
||||
|
||||
const refusal = goldenAcceptanceRefusal(report.results, declared, declaredNew);
|
||||
if (refusal) {
|
||||
console.error(refusal);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const silent = goldenSilentDeclarations(report.results, declared);
|
||||
if (silent.length) {
|
||||
console.log(`Объявлены как изменённые, но совпали с эталоном: ${silent.join(', ')}.`);
|
||||
}
|
||||
// Новые эталоны печатаются отдельной строкой, а не в общем списке: раньше они
|
||||
// растворялись среди изменившихся, и три кадра каталога устройств уехали в
|
||||
// контракт незамеченными (#350).
|
||||
const fresh = (report.results || []).filter((result) => result.status === 'missing-baseline')
|
||||
.map((result) => result.id).sort();
|
||||
const changed = (report.results || []).filter((result) => result.status === 'different')
|
||||
.map((result) => result.id).sort();
|
||||
console.log(changed.length
|
||||
? `Будут заменены эталоны: ${changed.join(', ')}.`
|
||||
: 'Ни один существующий эталон не изменился.');
|
||||
if (fresh.length) console.log(`СТАНУТ КОНТРАКТОМ ВПЕРВЫЕ: ${fresh.join(', ')}.`);
|
||||
if (!changed.length && !fresh.length) console.log('Будет перезаписан только манифест.');
|
||||
console.log(`Съёмка: chromium ${report.chromium || '?'}, матрица ${report.matrixVersion}.`);
|
||||
|
||||
const accept = spawnSync(process.execPath, [
|
||||
resolve(ROOT, 'demo/golden/accept.mjs'), '--reviewed', `--from=${from}`,
|
||||
resolve(ROOT, 'demo/golden/accept.mjs'), ...argv,
|
||||
], { cwd: ROOT, stdio: 'inherit' });
|
||||
process.exit(accept.status ?? 1);
|
||||
|
||||
@@ -98,3 +98,46 @@ export const goldenAcceptanceRefusal = (results, declared = [], declaredNew = []
|
||||
export const goldenSilentDeclarations = (results, declared = []) => declared
|
||||
.filter((id) => results.find((result) => result.id === id)?.status === 'passed')
|
||||
.sort();
|
||||
|
||||
/**
|
||||
* План замены: что переписать, что оставить как было (#351).
|
||||
*
|
||||
* Чистая функция, потому что решение здесь одно и ошибка в нём дорога:
|
||||
* `passed` означает «в пределах порога», а не «байт в байт». Прежняя приёмка
|
||||
* копировала кандидата поверх КАЖДОГО эталона, поэтому подпороговый дрейф уезжал
|
||||
* в контракт молча и накапливался: каждая приёмка подтягивала эталон к последней
|
||||
* среде, порог не пересекался никогда, а эталон уходил. Так `1e341c60` заменил
|
||||
* 22 картинки, объявив четыре.
|
||||
*
|
||||
* Необъявленная сцена сохраняет и файл, и свой хеш из прежнего индекса. Хеша нет
|
||||
* только у сцены без эталона, а такая обязана быть названа в `--expect-new` —
|
||||
* поэтому его отсутствие здесь ошибка, а не повод взять кандидата.
|
||||
*/
|
||||
export const goldenAcceptancePlan = ({
|
||||
scenarioIds, results, previousHashes = {}, declared = [], declaredNew = [],
|
||||
}) => {
|
||||
const accepted = new Set([...declared, ...declaredNew].filter(Boolean));
|
||||
const byId = new Map((results || []).map((result) => [result.id, result]));
|
||||
const replace = [];
|
||||
const keep = [];
|
||||
const hashes = {};
|
||||
for (const id of scenarioIds) {
|
||||
if (accepted.has(id)) {
|
||||
const digest = byId.get(id)?.actualSha256;
|
||||
if (typeof digest !== 'string' || !digest) {
|
||||
throw new Error(`объявленная сцена без хеша кандидата: ${id}`);
|
||||
}
|
||||
replace.push(id);
|
||||
hashes[id] = digest;
|
||||
continue;
|
||||
}
|
||||
const existing = previousHashes[id];
|
||||
if (typeof existing !== 'string' || !existing) {
|
||||
throw new Error(`необъявленная сцена без прежнего эталона: ${id};`
|
||||
+ ' назовите её в --expect-new');
|
||||
}
|
||||
hashes[id] = existing;
|
||||
keep.push(id);
|
||||
}
|
||||
return { replace, keep, hashes };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user