Documentation
¶
Overview ¶
cachedverdict.go — почему проверка дерева ОТКАЗЫВАЕТСЯ отвечать на прогоне, результат которого `go test` положит в кеш.
Предмет ¶
`go test` кеширует успешный результат пакета по содержимому самого пакета, его импортов и тех файлов и переменных окружения, к которым проба обратилась САМА. Проверка дерева нарушает эту посылку by construction: состав она берёт из индекса git ПОДПРОЦЕССОМ, а подпроцесс инструменту невидим. Правка в чужом каталоге кеш не инвалидирует — и над деревом, где проверка красная, печатается `ok (cached)`, отличимое от настоящего прохода ровно одним словом.
Следствие называется точно: вердикт становится функцией того, КОГДА его в последний раз считали.
Воспроизведение (2026-08-25, дерево вровень с origin/main) ¶
Прогрет гейт латинских имён; в каталог чужого сервиса добавлен отслеживаемый файл Go с кириллическим идентификатором. Обычный прогон — `ok (cached)`; тот же прогон с `-count=1` — находка с координатой. Правка чужого каталога до проверки не доехала.
Почему ОТКАЗ, а не «объявить пробу некэшируемой» ¶
Штатного способа объявить пробу некэшируемой в языке НЕТ. Это измерено, а не вычитано: изолированный модуль, шесть форм обращения к внешнему состоянию, каждая прогрета и затем проверена добавлением чужого файла.
чтение переменной окружения кеш применён — не помогает
открытие файла ВНЕ корня модуля кеш применён — не помогает
перечисление состава подпроцессом кеш применён — ЭТО И ЕСТЬ ДЕФЕКТ
чтение уже прочитанного файла кеш сброшен, но только на правку
СОДЕРЖИМОГО, не на появление файла
обход каталогов (os.ReadDir) кеш сброшен, но состав тогда берётся с
ДИСКА, а не из индекса: `git add` уже
лежащего файла листинга не меняет, и
в отпечаток заезжают игнорируемые
каталоги — ровно то, ради устранения
чего написан этот пакет
файл с меткой времени в будущем кеш не применяется НИКОГДА — но требует
оставить такой файл ВНУТРИ корня модуля,
то есть записи пробы в дерево, из
которого она запущена (собственная
находка гейта); убранный за собой файл
снова кешируется, а меток времени git не
хранит — в свежем клоне механизм умирает
молча
Остаётся отказ, и он безопасен: `go test` кеширует ТОЛЬКО успешные результаты, поэтому красный прогон кеша не отравляет (проверено тремя прогонами подряд — исполнились все три, зелёный сосед в том же пакете остался кешируемым).
Дискриминатор — заявление САМОГО инструмента, а не наша реконструкция правил ¶
`go test` передаёт пробе флаг журнала обращений РОВНО ТОГДА, когда собирается положить результат в кеш: журнал нужен ему, чтобы посчитать отпечаток входа. Нет флага — не будет и кеша. Реконструировать правила кеширования (перечень «кешируемых» флагов) не требуется, и это важно: перечень принадлежит инструменту и может измениться, а флаг журнала есть его собственный вывод.
Проверено по семи осям на изолированном модуле — рядом с ожидаемым исходом стоит фактическое поведение кеша, то есть контроль в обе стороны:
go test ./пакет/ флаг ЕСТЬ и кеш применяется go test ./пакет/ -count=1 флага НЕТ и кеш не применяется go test ./пакет/ -count=2 флага НЕТ и кеш не применяется go test ./пакет/ -race флаг ЕСТЬ и кеш применяется go test ./пакет/ -timeout 5m флаг ЕСТЬ и кеш применяется go test ./пакет/ -bench=. флага НЕТ и кеш не применяется go test (режим текущего каталога) флага НЕТ и кеш не применяется
Последняя строка — та, ради которой дискриминатор выбран именно такой: восстановление правил по перечню флагов дало бы здесь ЛОЖНУЮ находку (флагов не задано ни одного, а кеш инструмент всё равно не применяет).
tree.go — состав дерева, разложенный для ОБХОДА: файлы плюс каталоги-предки.
Чем это отличается от Under ¶
`Under` отвечает на вопрос «какие файлы лежат под этим каталогом» и годится, когда проверка их просто читает. Обходчику этого мало: он идёт по каталогам и обязан уметь отсечь целое поддерево (`filepath.SkipDir`), а не фильтровать файлы поштучно — игнорируемая рабочая копия дерева весит сотни мегабайт, и читать её ради последующего отбрасывания незачем.
Почему это ЗДЕСЬ, а не в тестовом файле пакета гейтов ¶
Раскладка жила в `_test.go` пакета гейтов и потому была доступна ровно одному пакету. Полтора десятка гейтов на неё опирались, и вынести любой из них в соседний пакет было нельзя, не унеся с собой её же копию: помощник в тестовом файле — это дом на одну семью. Пока он там стоял, расщепление пакета гейтов упиралось не в предмет обхода, а в место объявления помощника.
Здесь у него дом: обычный пакет, который импортирует кто угодно. Тестовая обёртка (fatal вместо error) остаётся у каждого пакета своя и стоит двадцать строк — цена, за которую пакеты не связываются между собой ради помощника.
Package treecorpus отвечает на вопрос «какие файлы содержит этот репозиторий» — для проверок, вердикт которых обязан быть свойством КОММИТА, а не рабочего каталога.
Зачем это не filepath.Walk ¶
Проверка, собирающая свой корпус обходом диска, читает и то, чего в репозитории нет: рабочие копии агентов, распаковки чартов, сгенерированные каталоги, отчёты прогонов, локальные оверлеи. Все они перечислены в `.gitignore` — то есть автор дерева уже сказал, что частью его они не являются, — но обходу файловой системы об этом никто не сообщает.
Следствие ходит в обе стороны, и обе наблюдались вживую на этом дереве:
- ШУМ: файл, которого в репозитории нет и по построению быть не может, делает проверку красной. Тогда её перестают читать;
- ТИШИНА: то же самое наоборот. Место, которое проверка читает только на машине разработчика (распакованный вендорный артефакт, сгенерированный каталог), в свежем checkout отсутствует — и там она молчит именно тогда, когда обязана говорить.
Единица счёта — отслеживаемый git-элемент. Это не выбор вкуса: ровно то множество увидит свежий клон и CI.
Почему отказ, а не откат на обход ¶
Недоступность git — ОТКАЗ. Молчаливый откат «нет git — пойду по диску» вернул бы ровно тот дефект, ради которого пакет написан, и сделал бы это незаметно: на машине без git проверка продолжала бы «работать».
Синтетическое дерево, собранное самой проверкой во временном каталоге, репозиторием не является, спрашивать у него индекс нечего — там обход файловой системы законен и остаётся на месте вызова. Этот пакет про репозиторий.
Вердикт обязан быть свойством коммита И ЭТОГО ПРОГОНА ¶
Тот же довод замыкается со второй стороны. Обход диска делал бы вердикт свойством рабочего КАТАЛОГА; кеш `go test` делает его свойством ВРЕМЕНИ — того момента, когда вердикт последний раз считали. Поэтому конструкторы настоящего индекса отказываются отвечать на прогоне, результат которого пойдёт в кеш: разбор, замеры и дискриминатор — cachedverdict.go.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrEmptyCorpus = errors.New("под каталогом нет ни одного отслеживаемого файла")
ErrEmptyCorpus — под запрошенным каталогом нет НИ ОДНОГО отслеживаемого файла: каталога в индексе нет вовсе либо он целиком покрыт правилами игнорирования.
Сентинел нужен, чтобы вызывающий мог отличить это состояние от отказа самого git (недоступен, каталог вне репозитория). Различие несёт исход: «служба без каталога миграций» — законный пустой ответ, а «git не запустился» — отказ, который обязан дойти до КАЖДОГО спрашивающего. Сведение их в одну ветку (`if err != nil { files = nil }`) молча превращает второе в первое, то есть отдаёт «ноль находок» на «ноль прочитанного» — ровно тот класс, ради которого написан этот пакет.
Functions ¶
func CachedVerdictRefusal ¶
func CachedVerdictRefusal() string
CachedVerdictRefusal — текст отказа, если вердикт этого прогона пойдёт в кеш, и ПУСТАЯ строка иначе.
Отдаётся текстом, а не завершением процесса: место отказа выбирает вызывающий. Конструкторы этого пакета возвращают его ошибкой, пакет проверок — печатает из TestMain и выходит ненулём, потому что там ещё нет ни одной пробы, которой можно было бы сообщить.
func Glob ¶
Glob — индексный двойник filepath.Glob: те же образцы, тот же отбор, но состав берётся у git, а не с диска.
Зачем отдельная функция, а не Under ¶
`Under` рекурсивна, а `filepath.Glob` — нет: `dir/*.sql` это ОДИН уровень. Перевод сайта с `filepath.Glob(dir+"/*.sql")` на `UnderWithSuffix(dir, ".sql")` сменил бы вместе с источником корпуса и ВОПРОС, который сайт задаёт, — то есть починка одного класса протащила бы за собой необъявленную смену поведения в каждом переводимом месте. Здесь меняется ровно источник состава.
Отбор идёт `filepath.Match` по ПОЛНОМУ пути, поэтому `*` не переходит через разделитель — как и у filepath.Glob. Метасимвол в середине образца (`ui-future/*/Dockerfile`) поддержан: литеральной базой берётся приставка до ПЕРВОГО компонента с метасимволом, а дальше решает Match.
Чем отличается от filepath.Glob — названо, а не умолчано ¶
- КАТАЛОГИ не возвращаются. git версионирует файлы, каталога как объекта в индексе нет; образец, которым ищут каталог, здесь не сработает. Все переведённые сайты ищут файлы.
- ОТСУТСТВУЮЩАЯ база — отказ, а не тихий пустой ответ: filepath.Glob на несуществующем каталоге возвращает (nil, nil), и «каталог переехал» становится неотличимо от «ничего не подошло». Отказ несёт ErrEmptyCorpus, поэтому вызывающий, для которого пустая база законна (служба без каталога миграций), отличает её от недоступного git одной проверкой errors.Is — а не сведением обеих в `files = nil`.
Непустая база, под которой ничего не подошло, — законный ПУСТОЙ ответ, а не отказ: читать было что, просто не совпало.
func ListFilesProcesses ¶
func ListFilesProcesses() int64
ListFilesProcesses — число поднятий процесса git за прогон. Для проб.
func RunResultWillBeCached ¶
func RunResultWillBeCached() bool
RunResultWillBeCached — положит ли `go test` результат этого прогона в кеш.
В прод-бинаре всегда false: флаг журнала передаёт только `go test`, поэтому вопрос там беспредметен by construction.
func Under ¶
Under возвращает АБСОЛЮТНЫЕ пути отслеживаемых файлов под каталогом dir, отсортированные (детерминизм входа — часть контракта проверки).
dir обязан лежать внутри рабочего дерева git. Ошибка возвращается и в случае, когда git недоступен, и когда каталог вне репозитория: вызывающий обязан на этом остановиться, а не выдать «ноль находок».
Types ¶
type Tree ¶
type Tree struct {
// contains filtered or unexported fields
}
Tree — состав дерева: множество файлов и множество каталогов, в которых есть хоть один файл этого состава. Пути — от корня, слэш-разделённые.
func NewTree ¶
NewTree читает ИНДЕКС git и раскладывает его в два множества.
Недоступность git — ОТКАЗ, а не пропуск. Молчаливый откат «нет git — иду по диску» вернул бы ровно тот дефект, ради которого написан этот пакет, и сделал бы это невидимо: на машине без git проверка продолжала бы «работать», читая игнорируемые каталоги.
func ParseIndex ¶
ParseIndex — разбор вывода `git ls-files -z`. Отдельно от NewTree, чтобы инъекция могла подать синтетический ввод, не заводя репозитория.
func SyntheticTree ¶
SyntheticTree — состав СИНТЕТИЧЕСКОГО дерева, собранного самой проверкой во временном каталоге.
Такое дерево репозиторием не является, спрашивать у него индекс нечего, и обход файловой системы здесь — не откат, а единственный возможный авторитет. Конструктор ОТДЕЛЬНЫЙ намеренно: откат внутри NewTree был бы невидим, а отдельное имя вызывающий выбирает сам и осознанно.
func (*Tree) Count ¶
Count — сколько файлов прочитано. Вызывающие печатают это как перепись, чтобы «ноль находок» отличалось от «ноль прочитанного».
func (*Tree) Files ¶
Files — множество файлов состава. Отдаётся ссылкой на внутреннюю карту: вызывающие обходят её десятками тысяч раз за прогон, а копия на каждый обход стоила бы дороже всего остального вместе. Карта предназначена ТОЛЬКО для чтения; менять её значит менять состав дерева под ногами у соседнего гейта.
func (*Tree) HasDir ¶
HasDir — в каталоге (или ниже) есть хоть один файл состава. Каталог, о котором состав не знает, обходить незачем.
func (*Tree) SortedFiles ¶
SortedFiles — то же множество списком, отсортированным: детерминизм входа проверки — часть её контракта, а порядок обхода карты в Go случаен.