Help us shape the future of Go! Take the 2026 Go Developer Survey

treecorpus

package
v1.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

View Source
var ErrEmptyCorpus = errors.New("под каталогом нет ни одного отслеживаемого файла")

ErrEmptyCorpus — под запрошенным каталогом нет НИ ОДНОГО отслеживаемого файла: каталога в индексе нет вовсе либо он целиком покрыт правилами игнорирования.

Сентинел нужен, чтобы вызывающий мог отличить это состояние от отказа самого git (недоступен, каталог вне репозитория). Различие несёт исход: «служба без каталога миграций» — законный пустой ответ, а «git не запустился» — отказ, который обязан дойти до КАЖДОГО спрашивающего. Сведение их в одну ветку (`if err != nil { files = nil }`) молча превращает второе в первое, то есть отдаёт «ноль находок» на «ноль прочитанного» — ровно тот класс, ради которого написан этот пакет.

Functions

func CachedVerdictRefusal

func CachedVerdictRefusal() string

CachedVerdictRefusal — текст отказа, если вердикт этого прогона пойдёт в кеш, и ПУСТАЯ строка иначе.

Отдаётся текстом, а не завершением процесса: место отказа выбирает вызывающий. Конструкторы этого пакета возвращают его ошибкой, пакет проверок — печатает из TestMain и выходит ненулём, потому что там ещё нет ни одной пробы, которой можно было бы сообщить.

func Glob

func Glob(pattern string) ([]string, error)

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

func Under(dir string) ([]string, error)

Under возвращает АБСОЛЮТНЫЕ пути отслеживаемых файлов под каталогом dir, отсортированные (детерминизм входа — часть контракта проверки).

dir обязан лежать внутри рабочего дерева git. Ошибка возвращается и в случае, когда git недоступен, и когда каталог вне репозитория: вызывающий обязан на этом остановиться, а не выдать «ноль находок».

func UnderWithSuffix

func UnderWithSuffix(dir string, suffixes ...string) ([]string, error)

UnderWithSuffix — то же, но оставляет только пути с одним из суффиксов. Пустой набор суффиксов означает «все файлы».

Types

type Tree

type Tree struct {
	// contains filtered or unexported fields
}

Tree — состав дерева: множество файлов и множество каталогов, в которых есть хоть один файл этого состава. Пути — от корня, слэш-разделённые.

func NewTree

func NewTree(root string) (*Tree, error)

NewTree читает ИНДЕКС git и раскладывает его в два множества.

Недоступность git — ОТКАЗ, а не пропуск. Молчаливый откат «нет git — иду по диску» вернул бы ровно тот дефект, ради которого написан этот пакет, и сделал бы это невидимо: на машине без git проверка продолжала бы «работать», читая игнорируемые каталоги.

func ParseIndex

func ParseIndex(root string, nulSeparated []byte) *Tree

ParseIndex — разбор вывода `git ls-files -z`. Отдельно от NewTree, чтобы инъекция могла подать синтетический ввод, не заводя репозитория.

func SyntheticTree

func SyntheticTree(root string) (*Tree, error)

SyntheticTree — состав СИНТЕТИЧЕСКОГО дерева, собранного самой проверкой во временном каталоге.

Такое дерево репозиторием не является, спрашивать у него индекс нечего, и обход файловой системы здесь — не откат, а единственный возможный авторитет. Конструктор ОТДЕЛЬНЫЙ намеренно: откат внутри NewTree был бы невидим, а отдельное имя вызывающий выбирает сам и осознанно.

func (*Tree) Count

func (t *Tree) Count() int

Count — сколько файлов прочитано. Вызывающие печатают это как перепись, чтобы «ноль находок» отличалось от «ноль прочитанного».

func (*Tree) Files

func (t *Tree) Files() map[string]bool

Files — множество файлов состава. Отдаётся ссылкой на внутреннюю карту: вызывающие обходят её десятками тысяч раз за прогон, а копия на каждый обход стоила бы дороже всего остального вместе. Карта предназначена ТОЛЬКО для чтения; менять её значит менять состав дерева под ногами у соседнего гейта.

func (*Tree) HasDir

func (t *Tree) HasDir(rel string) bool

HasDir — в каталоге (или ниже) есть хоть один файл состава. Каталог, о котором состав не знает, обходить незачем.

func (*Tree) HasFile

func (t *Tree) HasFile(rel string) bool

HasFile — файл входит в состав.

func (*Tree) Root

func (t *Tree) Root() string

Root — каталог, о котором говорит этот состав.

func (*Tree) SortedFiles

func (t *Tree) SortedFiles() []string

SortedFiles — то же множество списком, отсортированным: детерминизм входа проверки — часть её контракта, а порядок обхода карты в Go случаен.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL