mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
fix: pin the compositor so a frame depends only on the commit (#424)
User-Visible: no Issue: #424
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Аргументы запуска Chromium для съёмки документации (#410, #424).
|
||||
*
|
||||
* Модуль отдельный и чистый намеренно: `capture.mjs` — исполняемый скрипт,
|
||||
* который при импорте поднимает браузер и снимает набор. Тест, желающий
|
||||
* прочитать эти константы, не должен запускать съёмку внутри `npm test` — тот
|
||||
* же приём и та же причина, что у `clip.mjs`.
|
||||
*
|
||||
* Первые три флага фиксируют растеризацию текста и цветовой профиль: именно они
|
||||
* убрали основную часть дрейфа в #410.
|
||||
*
|
||||
* Последние два добавлены в #424 и обязаны стоять вместе — по отдельности ни
|
||||
* один дрейф не убирает (проверено перебором, восемь прогонов на конфигурацию):
|
||||
*
|
||||
* - `--disable-partial-raster` запрещает переиспользовать ранее нарисованные
|
||||
* куски тайла. С включённой частичной растеризацией кадр зависит от того,
|
||||
* что композитор рисовал ДО него, то есть от истории прогона;
|
||||
* - `--run-all-compositor-stages-before-draw` заставляет пройти все стадии
|
||||
* композитора до отрисовки, иначе снимок берётся на полпути и «докрутка»
|
||||
* каждый раз происходит по-своему.
|
||||
*
|
||||
* Подпись дефекта, ради которого это сделано: один-два случайных кадра из
|
||||
* десяти расходятся между прогонами на единицы пикселей и два уровня, при этом
|
||||
* внутри одного процесса всё стабильно.
|
||||
*/
|
||||
export const DETERMINISTIC_ARGS = [
|
||||
'--force-color-profile=srgb',
|
||||
'--font-render-hinting=none',
|
||||
'--disable-lcd-text',
|
||||
'--disable-partial-raster',
|
||||
'--run-all-compositor-stages-before-draw',
|
||||
];
|
||||
@@ -13,6 +13,7 @@ import { assertFreshDemoBundle } from '../bundle-freshness.mjs';
|
||||
import { goldenClip, prepareGoldenScenario } from '../golden/harness.mjs';
|
||||
import { launch } from '../serve.mjs';
|
||||
import { DOC_SCREENSHOT_VERSION, DOC_SCREENSHOTS } from './screenshots.mjs';
|
||||
import { DETERMINISTIC_ARGS } from './browser-args.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const OUTPUT = resolve(ROOT, 'docs/images');
|
||||
@@ -167,14 +168,11 @@ mkdirSync(OUTPUT, { recursive: true });
|
||||
// 09-device-info). Дельта — единицы уровней в RGB на сглаженных границах, alpha
|
||||
// не менялась: подпись субпиксельного сглаживания, а не изменения продукта.
|
||||
//
|
||||
// `--disable-lcd-text` убирает субпиксельное сглаживание (оно и плавало),
|
||||
// `--font-render-hinting=none` снимает зависимость от хинтинга,
|
||||
// `--force-color-profile=srgb` фиксирует профиль. `reducedMotion: 'reduce'`
|
||||
// добавлен к `animations: 'disabled'` у самого скриншота: первое гасит анимации
|
||||
// в CSS, второе — уже начатые переходы на момент съёмки.
|
||||
const DETERMINISTIC_ARGS = [
|
||||
'--force-color-profile=srgb', '--font-render-hinting=none', '--disable-lcd-text',
|
||||
];
|
||||
// Аргументы запуска и объяснение каждого — в `browser-args.mjs`: константы
|
||||
// живут отдельно, чтобы тест мог их прочитать, не поднимая браузер (#424).
|
||||
// `reducedMotion: 'reduce'` добавлен к `animations: 'disabled'` у самого
|
||||
// скриншота: первое гасит анимации в CSS, второе — уже начатые переходы на
|
||||
// момент съёмки.
|
||||
/**
|
||||
* Режим замера стабильности (#410): `node demo/docs/capture.mjs --stability=3`.
|
||||
*
|
||||
|
||||
@@ -3552,3 +3552,17 @@ require hands on real hardware — they remain for the human pass.
|
||||
attachment with Retry, Copy message, Download and manual links. Old or
|
||||
mismatched backend leaves About/Guide usable but exposes no fake submit
|
||||
[pre-beta: success/429/timeout/unknown-command smokes in light/dark].
|
||||
|
||||
## Съёмка документации запускается с флагами детерминизма (#424)
|
||||
|
||||
- [ ] `demo/docs/browser-args.mjs` содержит `--disable-partial-raster` и
|
||||
`--run-all-compositor-stages-before-draw` — оба, а не один. По отдельности
|
||||
ни один дрейф не убирает: проверено перебором, восемь прогонов на
|
||||
конфигурацию.
|
||||
|
||||
Первый запрещает переиспользовать ранее нарисованные куски тайла (иначе кадр
|
||||
зависит от того, что композитор рисовал до него), второй заставляет пройти все
|
||||
стадии композитора до отрисовки (иначе снимок берётся на полпути). Подпись
|
||||
дефекта, если он вернётся: один-два случайных кадра из десяти расходятся между
|
||||
прогонами на единицы пикселей и два уровня, а `--stability=3` при этом зелёный —
|
||||
внутри одного процесса всё стабильно.
|
||||
|
||||
@@ -933,6 +933,29 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " this._persistDecorStyle();\n }, 0);",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'capture-allows-partial-raster',
|
||||
guard: 'node --test test/capture-determinism-args.test.mjs',
|
||||
because: 'partial raster reuses whatever the compositor drew before, so the frame '
|
||||
+ 'depends on the history of the run — one or two of ten frames drifted between '
|
||||
+ 'runs while three shots inside one process matched byte for byte (#424)',
|
||||
patches: [{
|
||||
file: 'demo/docs/browser-args.mjs',
|
||||
find: " '--disable-partial-raster',\n",
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'capture-draws-before-compositor-settles',
|
||||
guard: 'node --test test/capture-determinism-args.test.mjs',
|
||||
because: 'without running every compositor stage first the screenshot is taken '
|
||||
+ 'mid-flight, and the remaining work lands differently on every run (#424)',
|
||||
patches: [{
|
||||
file: 'demo/docs/browser-args.mjs',
|
||||
find: " '--run-all-compositor-stages-before-draw',\n",
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'danger-confirm-back-into-the-branch',
|
||||
guard: 'node demo/smoke_danger_confirm_branches.mjs',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { DETERMINISTIC_ARGS } from '../demo/docs/browser-args.mjs';
|
||||
|
||||
// #424. Съёмка документации давала разные кадры на одном коммите: один-два
|
||||
// случайных кадра из десяти расходились между прогонами, при этом внутри
|
||||
// процесса всё было стабильно. Причина — композитор: частичная растеризация
|
||||
// переиспользует ранее нарисованные куски тайла, а снимок берётся до конца всех
|
||||
// стадий. Два флага чинят это только вместе; по отдельности ни один не помогает
|
||||
// (проверено перебором, восемь прогонов на конфигурацию).
|
||||
|
||||
test('набор запрещает частичную растеризацию', () => {
|
||||
assert.ok(DETERMINISTIC_ARGS.includes('--disable-partial-raster'),
|
||||
'без этого флага кадр зависит от того, что композитор рисовал до него');
|
||||
});
|
||||
|
||||
test('набор требует пройти все стадии композитора до отрисовки', () => {
|
||||
assert.ok(DETERMINISTIC_ARGS.includes('--run-all-compositor-stages-before-draw'),
|
||||
'без этого флага снимок берётся на полпути, и докрутка каждый раз своя');
|
||||
});
|
||||
|
||||
test('прежние флаги детерминизма текста остались на месте', () => {
|
||||
// #410 убрал ими основную часть дрейфа; потерять их вместе с правкой #424
|
||||
// означало бы разменять одну недетерминированность на другую.
|
||||
for (const flag of ['--force-color-profile=srgb', '--font-render-hinting=none', '--disable-lcd-text']) {
|
||||
assert.ok(DETERMINISTIC_ARGS.includes(flag), flag);
|
||||
}
|
||||
});
|
||||
|
||||
test('съёмка берёт набор из этого модуля, а не объявляет свой', () => {
|
||||
// Иначе тест стерёг бы константу, которой никто не пользуется.
|
||||
const source = readFileSync(new URL('../demo/docs/capture.mjs', import.meta.url), 'utf8');
|
||||
assert.match(source, /import \{ DETERMINISTIC_ARGS \} from '\.\/browser-args\.mjs'/);
|
||||
assert.doesNotMatch(source, /const DETERMINISTIC_ARGS\s*=/,
|
||||
'собственное объявление в capture.mjs разошлось бы с проверяемым набором');
|
||||
});
|
||||
Reference in New Issue
Block a user