treecorpus

package
v1.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 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