Chrome for Testing: фиксируем build, а не отпечаток
Endpoint доступности публикует четыре канала и три артефакта на платформу в одной версии. Фиксация версии оставляет вне её шрифты, GPU и часы.
BotBrowser Team
Нужна структурированная документация по теме Отпечатки?
Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.
Зафиксированный build это самая дешёвая победа в воспроизводимости, и она меньше, чем кажется. BotBrowser предоставляет объявленный профиль, который сохраняет согласованность поверхностей, доступных странице, между запусками, и не определяет, какой бинарник установлен. Прямое чтение endpoint Chrome for Testing показывает четыре канала с версиями, шесть платформ, публикующих браузер, драйвер и headless shell в одной версии, а затем список значений, которые продолжают меняться после фиксации версии.
TL;DR
- Endpoint доступности публикует четыре канала, Stable, Beta, Dev и Canary, у каждого есть версия и ревизия.
- На дату чтения Stable был
157.0.8103.50с ревизией1717349, а Canary был159.0.8154.2с ревизией1739869. - Каждая платформа публикует три артефакта в одной версии: браузер,
chromedriverиchrome-headless-shell. - Покрыто шесть платформ:
linux-arm64,linux64,mac-arm64,mac-x64,win32иwin64, и все три артефакта есть для каждой. - Каждый опубликованный URL для версии несёт ту же строку версии, и именно это делает фиксацию осмысленной.
- Список известных хороших сборок содержал 2626 версий от мажора 113 до 159 на момент чтения.
- Фиксация build исправляет рендерер и драйвер вместе, и она не исправляет шрифты, драйверы GPU, системные библиотеки и часы.
- Профиль описывает, что сообщает окружение; зафиксированный build описывает, что было установлено. Нужны оба, и ни один не заменяет другой.
Содержание
- Что публикуют endpoint
- Три артефакта, одна версия
- Что фиксация исправляет и что нет
- Практический итог
Что публикуют endpoint
Первый полезный факт: доступность публикуется как данные, а не как страница загрузки, и именно это делает автоматическую фиксацию возможной. Два endpoint несут почти всё, что нужно pipeline: один сообщает последний известный хороший build по каждому каналу, другой сообщает все известные хорошие версии.
last_known_good_channels: Stable, Beta, Dev, Canary
Stable: version=157.0.8103.50 revision=1717349
Beta: version=158.0.8127.18 revision=1727472
Dev: version=159.0.8144.0 revision=1735750
Canary: version=159.0.8154.2 revision=1739869
Важны два поля, и они отвечают на разные вопросы. Версия это строка, которую узнаёт читатель, а ревизия это позиция коммита в репозитории Chromium. Фиксация по версии читаема; фиксация по ревизии однозначна, даже если канал переопубликует сборку. Записывать оба поля это дешёвый вариант, а записывать только версию это распространённая ошибка.
Структура каналов важна и для pipeline, которому нужна стабильность без устаревания. Stable движется медленно и является правильным выбором по умолчанию для release-гейта. Beta, Dev и Canary движутся быстрее и полезны, чтобы обнаружить изменение до того, как оно дойдёт до Stable, а это противоположно тому, чего требует требование воспроизводимости. Pipeline, который следит за Canary ради детерминизма, выбрал самый подвижный канал для работы, которой нужен самый медленный.
known_good_count: 2626
oldest: 113.0.5672.0 newest: 159.0.8154.2
majors_present (last six): 154, 155, 156, 157, 158, 159
Размер этого списка это практический аргумент в пользу программного чтения. Две тысячи шестьсот версий это не число, которое человек просматривает глазами, а фиксация, отредактированная вручную, расходится в тот момент, когда кто-то копирует версию из опубликованной заметки вместо endpoint.
Три артефакта, одна версия
Второй факт ломает большинство pipeline, которые фиксируют только браузер. Каждая версия публикует три отдельных артефакта, и рабочая установка автоматизации требует согласия всех трёх.
chrome_platforms: linux-arm64, linux64, mac-arm64, mac-x64, win32, win64
mac-arm64: chrome=true driver=true shell=true
linux64: chrome=true driver=true shell=true
win64: chrome=true driver=true shell=true
distinct_versions_in_urls: 157.0.8103.50
all_artefacts_agree: true
Последняя строка несёт основную нагрузку. Каждый опубликованный URL для версии несёт ту же строку версии, поэтому фиксация одного значения фиксирует набор. Когда три расходятся, отказ не случаен: драйвер новее своего браузера отказывается подключаться, а headless shell из другой сборки рендерит иначе, чем браузер, который он должен представлять.
Headless shell это артефакт, который команды забывают, и именно он тихо меняет результаты. Тест, который идёт против chrome-headless-shell, идёт не против бинарника браузера, поэтому фиксация браузера при свободном shell даёт pipeline, где рендерер меняется без изменения номера версии. Проверка, что все три сообщают одну версию, это утверждение в одну строку, и оно ловит случай, который фиксация только браузера поймать не может.
pin = "157.0.8103.50"
assert browser_version == pin
assert driver_version == pin
assert shell_version == pin
Та же структура объясняет отказ, похожий на нестабильность. Образ контейнера, который ставит браузер из одного источника, а драйвер из менеджера пакетов, может получить две версии одного мажора, которые в основном работают и периодически отказывают на единственной изменившейся возможности. Утверждение выше превращает этот периодический отказ в ошибку запуска, и это тот результат, за который стоит платить.
Что фиксация исправляет и что нет
Третий факт решает, достаточно ли фиксации, и честный ответ: она исправляет меньше, чем надеется читатель. Зафиксированный build исправляет рендерер и драйвер вместе, потому что это артефакты, которые он называет.
pinned by the build: renderer version, driver version, headless shell version
not pinned: fonts, GPU driver, system libraries, time zone, clock
Шрифты это самый частый остаток. Контейнер с зафиксированным браузером и без шрифта CJK рендерит китайскую страницу прямоугольниками, и результат в пикселях отличается от машины разработчика с той же версией браузера. Ничто в наборе артефактов это не исправляет, потому что шрифты приходят из образа операционной системы, а не из пакета браузера.
Стек GPU второй. Зафиксированный браузер на машине с программным рендерингом даёт другой вывод canvas и WebGL, чем тот же браузер на машине с реальным адаптером, и оба одной версии. Эта разница измерима, и это не дефект фиксации; это переменная, которую фиксация никогда не покрывала.
Часы и часовой пояс третьи, и их проще всего упустить, потому что они вообще не часть браузера. Тест, который читает отформатированную дату или строку, зависящую от локали, может пройти и упасть на одной и той же сборке в зависимости от конфигурации хоста, а отчёт, который записывает только версию браузера, не может объяснить разницу.
Практическое чтение таково: фиксация это необходимый первый шаг и плохая точка остановки, тот же вывод, к которому приходит детерминированный режим со стороны планирования. Она убирает самый большой источник дрейфа, то есть рендерер, меняющийся под фиксированным тестом, и оставляет три меньших источника, которые запись должна назвать отдельно. Pipeline, который записывает build и больше ничего, может ответить «изменился ли браузер» и не может ответить «изменилось ли окружение».
Четвёртое наблюдение уместно здесь, потому что именно оно превращает фиксацию в операционную рутину. Каналы движутся по своему расписанию, поэтому фиксация, которую никогда не пересматривают, становится решением перестать получать исправления. Рутина, которая держит фиксацию честной, состоит из трёх шагов, и ни один не автоматичен: прочитать endpoint, сравнить опубликованную версию Stable с зафиксированной и решить, является ли разница исправлением безопасности, которое нужно взять, или изменением функции, которое может ждать следующего планового окна.
pinned: 157.0.8103.50
published:157.0.8103.50
delta: none
action: none
Это сравнение дешёвое, и именно этот шаг команды пропускают. Pipeline, который фиксирует и никогда не сравнивает, в итоге будет работать на сборке с известным дефектом, пока панель показывает здоровый номер версии, потому что версия зафиксирована и поэтому выглядит намеренной. Запись опубликованного значения рядом с зафиксированным делает дрейф видимым, не требуя, чтобы кто-то помнил, когда фиксация была написана.
Есть вторая причина сохранять сравнение. Скачок мажорной версии меняет рендерер так, как скачок патча не меняет, и фиксация, пересекающая мажор, это другое решение, чем фиксация, берущая патч. Отношение к обоим как к «обновлению» сливает изменение с низким риском и изменение с высоким риском в одно действие, и последующий обзор оказывается либо слишком тяжёлым для патча, либо слишком лёгким для мажора.
Сама строка версии несёт эту информацию, и читатель может использовать её без changelog. Четыре компонента соответствуют полям, которые Chromium хранит в chrome/VERSION: MAJOR.MINOR.BUILD.PATCH, так что 157.0.8103.50 читается как мажор 157, минор 0, build 8103 и патч 50. Изменение первого компонента заслуживает полной новой базовой линии; изменение последнего обычно наименее разрушительно. Чтение строки вместо отношения к ней как к непрозрачному токену делает это суждение возможным до любой загрузки.
Ничего из этого не требует нового инструмента. Endpoint это JSON, сравнение это равенство строк, а утверждение это три проверки при запуске. Требуется решение записывать build рядом с профилем, потому что отчёт, который называет одно и не называет другое, не может ответить на вопрос, который читатель действительно задаст.
Практический итог
Читайте endpoint доступности, а не опубликованную заметку, записывайте версию и ревизию вместе и утверждайте при запуске, что браузер, chromedriver и chrome-headless-shell сообщают зафиксированное значение. Отслеживайте Stable для release-гейта и оставьте быстрые каналы для обнаружения изменений, потому что требование детерминизма и требование свежести тянут в противоположные стороны. Затем записывайте то, что фиксация не покрывает: набор шрифтов, стек GPU, системные библиотеки и часы. О контракте выпуска, который это реализует, читайте в фиксация и обновление версий браузерной автоматизации; о том, почему две машины на одной сборке всё равно расходятся, читайте в расхождение отпечатков узлов Selenium Grid.
Объявленный профиль и зафиксированный build отвечают на два разных вопроса. Профиль говорит, что окружение сообщает странице; build говорит, что было установлено. Установка, у которой есть одно и нет другого, может быть согласованной в одном измерении и дрейфовать в другом, и только отчёт, называющий оба, можно проверить позже.
Последняя группа наблюдений касается назначения трёх артефактов, потому что читатель, который считает их взаимозаменяемыми, зафиксирует не тот. Бинарник браузера это рендерер и всё, что из него следует: вёрстка, поставляемые с ним шрифты, движок JavaScript и поверхности, доступные странице. Драйвер это мост протокола, и его версия должна совпадать, потому что протокол, на котором он говорит, генерируется из собственного определения протокола браузера.
browser -> renderer, layout, JS engine, page-readable surfaces
driver -> protocol bridge; must match the browser it drives
shell -> headless renderer used instead of the browser binary
Средняя строка объясняет, почему несовпадение драйвера отказывает громко, а не тонко. Драйвер, собранный против другого поколения протокола, откажет в сессии или упадёт на первой команде, чья форма изменилась, и ошибка назовёт команду, а не версию. Читатель, который видит ошибку протокола и похожую на совпадающую строку версии, обычно смотрит на драйвер, установленный менеджером пакетов, а не из набора артефактов.
Третья строка даёт тихие различия. Headless shell это отдельный бинарник, поэтому тест, который идёт против него, вообще не идёт против браузера, даже когда строки версий совпадают. Оба рендерят одну страницу немного по-разному именно там, где это замечает визуальное сравнение: субпиксельные позиции текста, порядок подстановки шрифтов и способ декодирования изображения с отложенной загрузкой. Pipeline, который фиксирует браузер и оставляет shell свободным, зафиксировал артефакт, который не использует, и оставил незафиксированным тот, который использует.
Есть ещё одно следствие для отчёта об отпечатках. Значение, прочитанное со страницы, зависит от рендерера, поэтому отчёт, который называет версию браузера и не называет версию shell, назвал не тот артефакт всякий раз, когда запуск использовал shell. На практике это не тонкое различие: это разница между отчётом, который можно воспроизвести, и тем, который нельзя.
Практическое правило короткое. Фиксируйте три вместе, утверждайте три при запуске и записывайте, какой из трёх действительно работал. Pipeline, который делает эти три вещи, может ответить на вопрос, который задаст поздний читатель: не «какая версия была зафиксирована», а «какой бинарник дал этот результат».
Замечание о самих двух endpoint, потому что их имена легко спутать, и разница решает, как писать фиксацию. Один endpoint отвечает «какой текущий build у этого канала», и он меняется всякий раз, когда канал переопубликован. Другой отвечает «какие версии существуют», и он только растёт. Фиксация, написанная по первому endpoint, это снимок движущегося значения; фиксация по второму это ссылка на неизменяемую запись, что и нужно воспроизводимой сборке.
last-known-good-versions.json -> current per channel; changes on republish
known-good-versions.json -> every published version; only grows
<version>.json -> the artefact list for one version; immutable
Третью строку pipeline должен читать при установке. Документ индексирован строкой версии, поэтому фиксация разрешается в один документ, который называет каждый артефакт для каждой платформы, и разрешение не зависит от того, какой канал сейчас здоров. Pipeline, который читает первый endpoint при установке, поставит другую сборку на следующей неделе без любого изменения конфигурации, а это противоположно назначению фиксации.
Различие объясняет и класс запутанных отчётов об ошибках. Набор тестов, который прошёл вчера и падает сегодня без изменения кода, обычно читает подвижный endpoint где-то в подготовке, и исправление не в том, чтобы гнаться за отказом, а в том, чтобы сделать чтение неизменяемым. Это изменение в одну строку, и оно убирает целую категорию периодических отказов из записи.
Команде, которой также нужно знать, когда двигаться, первый endpoint всё равно подходит для чтения, просто в другое время. Его место в плановой проверке, которая сообщает дельту, а не в пути установки, который должен быть воспроизводимым. Разделение двух чтений по назначению позволяет pipeline быть одновременно воспроизводимым и актуальным, что кажется противоречием только до момента разделения.
Ещё одно следствие вытекает из разделения артефактов, и оно касается того, что запись должна содержать до аудита. Идентификатор сборки отвечает, какой рендерер дал результат. Он не отвечает, какие шрифты были доступны, какой графический адаптер присутствовал и какие часы прочитал процесс. Эти три переменные превращают идентичную сборку в другой результат, и ни одной из них нет в наборе артефактов по замыслу.
record: build, fonts present, graphics adapter, time zone
missing any one -> the result is reproducible only by luck
Самый дешёвый способ держать эту запись это писать её при запуске, а не восстанавливать позже. Строка запуска с названием сборки, длиной списка шрифтов, строкой адаптера и зоной America/Toronto стоит несколько сотен байт и отвечает на вопрос, который читатель задаст после изменения результата. Восстанавливать её потом значит угадывать по образу, который могли пересобрать с тех пор, а это дорогая версия той же работы.
Случай шрифтов заслуживает отдельной фразы, потому что он даёт неверный ответ, а не отказ. Отсутствующий шрифт CJK не поднимает ошибку; он подставляет резервный шрифт, страница всё равно рендерится, извлечённый текст остаётся верным, а снимок отличается. Pipeline, который сравнивает снимки, видит разницу, которую не может объяснить, а pipeline, который сравнивает извлечённый текст, не видит ничего. Запись количества шрифтов превращает оба случая в наблюдаемое условие.
Вот форма всей темы. Фиксация убирает самую большую переменную и оставляет небольшой набор других, и ценность работы в том, чтобы точно назвать остаток, а не делать вид, что фиксация покрыла всё. Читатель, который записывает четыре поля, может объяснить изменение; читатель, который записывает одно, может только сообщить, что что-то сдвинулось.
Последняя операционная заметка закрывает вопрос о смысле фиксации. Сборку фиксируют не потому, что изменение плохо, а потому, что необъявленное изменение делает сбой необъяснимым. Фиксация покупает атрибуцию.
unpinned + failure -> investigate the browser, the fonts, the GPU, the code
pinned + failure -> compare against the last run on the same build
Источники
Эта статья входит в руководство: Что такое отпечаток браузера?
Похожие статьи
Фиксация и обновление версий браузерной автоматизации
ПлатформаChrome headless новый и старый: shell, режим и различия
ОтпечаткиЧто на самом деле проверяют публичные наборы тестов
Начало работыSelenium без окна: ChromeOptions и различия
Начало работыUndetected ChromeDriver: альтернативы на уровне браузера
ОтпечаткиRebrowser Patches: что меняется после настройки прокси?
Выполните ту же проверку в BotBrowser
Загрузите профиль, сохраните внутри браузера те сигналы, о которых только что прочитали, и подтвердите результат на своей машине до раскатки.