Give the battery a per-run log and refuse to report on a run whose log is missing, with the guard's exit code held to a non-zero literal
This commit is contained in:
parent
7f4cf41864
commit
201363b2a7
5 changed files with 314 additions and 70 deletions
2
platform/.gitignore
vendored
2
platform/.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
# Рабочий артефакт `make check`: рецепт пишет его в корень зоны и сам удаляет,
|
# Рабочий артефакт `make check`: рецепт пишет его в корень зоны и сам удаляет,
|
||||||
# но прерванный прогон оставляет — а коммитит зону оркестратор.
|
# но прерванный прогон оставляет — а коммитит зону оркестратор.
|
||||||
.check.log
|
.check.log*
|
||||||
|
|
|
||||||
|
|
@ -117,23 +117,42 @@ conditions:
|
||||||
# It also prints the register gate's findings (internal/gates, `ALARM` lines): open rows whose weight
|
# It also prints the register gate's findings (internal/gates, `ALARM` lines): open rows whose weight
|
||||||
# says minor and whose words say money, silence or a hold — the class a reconnaissance that counts
|
# says minor and whose words say money, silence or a hold — the class a reconnaissance that counts
|
||||||
# only `major` never sees.
|
# only `major` never sees.
|
||||||
|
#
|
||||||
|
# ⛔ THE LOG IS PER-RUN AND ITS ABSENCE IS RED, and both halves were bought by a defect. Every line
|
||||||
|
# this recipe prints is a GREP over that file, and grep answers a missing file exactly as it answers a
|
||||||
|
# clean one: with nothing. So the recipe used to reach its `else` and print «every test ran: no host
|
||||||
|
# condition was missing» over a run whose log had vanished — a bill of health for a measurement that
|
||||||
|
# never happened. Observed 06.09: three `grep: .check.log: No such file or directory` followed by that
|
||||||
|
# very line, on a run with FIVE skips.
|
||||||
|
#
|
||||||
|
# The cause was the FIXED name: two batteries in one directory — normal here, the zone and the
|
||||||
|
# orchestrator both run it — and whichever finished first deleted the other's evidence mid-recipe.
|
||||||
|
# `$$$$` is the shell's PID, so runs no longer share a file. The guard below is the second half and
|
||||||
|
# the one that matters: it covers every OTHER way a log can go missing (a failed redirect, a full
|
||||||
|
# disk, a hand), and it turns «nothing to read» into a failure instead of a clean bill. ⚠ The two are
|
||||||
|
# not redundant — the unique name removes today's cause, the guard removes the CLASS.
|
||||||
check: build vet fmt lint sqlc-check
|
check: build vet fmt lint sqlc-check
|
||||||
@$(GO) test ./... -race -count=1 -v > .check.log 2>&1; status=$$?; \
|
@log=.check.log.$$$$; \
|
||||||
grep -E '^(ok|FAIL|\?)' .check.log || true; \
|
$(GO) test ./... -race -count=1 -v > "$$log" 2>&1; status=$$?; \
|
||||||
if grep -q 'ALARM PD-' .check.log; then \
|
if [ ! -s "$$log" ]; then \
|
||||||
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
|
echo "--- THE BATTERY LEFT NO LOG ($$log): every line below would be a grep over a file that is not there ---"; \
|
||||||
grep -o 'ALARM PD-.*' .check.log; fi; \
|
echo "--- and a missing file is INDISTINGUISHABLE from a clean one to grep, so this exits RED rather than printing a bill of health nobody measured ---"; \
|
||||||
if [ $$status -ne 0 ]; then \
|
|
||||||
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' .check.log; \
|
|
||||||
echo "--- the log is kept at .check.log: the message under a failing test is where it says what to do ---"; \
|
|
||||||
exit 1; fi; \
|
exit 1; fi; \
|
||||||
if grep -q -- '--- SKIP' .check.log; then \
|
grep -E '^(ok|FAIL|\?)' "$$log" || true; \
|
||||||
echo "--- did NOT run: $$(grep -c -- '--- SKIP' .check.log) skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
|
if grep -q 'ALARM PD-' "$$log"; then \
|
||||||
|
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
|
||||||
|
grep -o 'ALARM PD-.*' "$$log"; fi; \
|
||||||
|
if [ $$status -ne 0 ]; then \
|
||||||
|
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' "$$log"; \
|
||||||
|
echo "--- the log is kept at $$log: the message under a failing test is where it says what to do ---"; \
|
||||||
|
exit 1; fi; \
|
||||||
|
if grep -q -- '--- SKIP' "$$log"; then \
|
||||||
|
echo "--- did NOT run: $$(grep -c -- '--- SKIP' "$$log") skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
|
||||||
$(MAKE) --no-print-directory -s conditions; \
|
$(MAKE) --no-print-directory -s conditions; \
|
||||||
echo "--- skipped tests ---"; \
|
echo "--- skipped tests ---"; \
|
||||||
grep -- '--- SKIP' .check.log; \
|
grep -- '--- SKIP' "$$log"; \
|
||||||
else echo "--- every test ran: no host condition was missing ---"; fi; \
|
else echo "--- every test ran: no host condition was missing ---"; fi; \
|
||||||
rm -f .check.log
|
rm -f "$$log"
|
||||||
|
|
||||||
# Not in `check`: fuzzing is time-boxed exploration, not a gate. The seed corpus runs as an
|
# Not in `check`: fuzzing is time-boxed exploration, not a gate. The seed corpus runs as an
|
||||||
# ordinary test on every `check`; this target is for going deeper on the decoder.
|
# ordinary test on every `check`; this target is for going deeper on the decoder.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -297,6 +297,58 @@ r.ordered_units is not null then` — считают полосу в ЮНИТА
|
||||||
у ре-прохода — не дефект: он купил ноль глав и доставил ноль, и `ordered_chapters: 0` рядом делает
|
у ре-прохода — не дефект: он купил ноль глав и доставил ноль, и `ordered_chapters: 0` рядом делает
|
||||||
это чтение однозначным; `null` там был бы ХУЖЕ, потому что `null` уже занят под «меряется в юнитах».
|
это чтение однозначным; `null` там был бы ХУЖЕ, потому что `null` уже занят под «меряется в юнитах».
|
||||||
|
|
||||||
|
### ⛔ ОТДЕЛЬНЫЙ АКТ 06.09 — ПРИБОР ПЕРЕСТАЛ УМЕТЬ ВЫДАВАТЬ ЧИСТУЮ СПРАВКУ О ТОМ, ЧЕГО НЕ ИЗМЕРЯЛ
|
||||||
|
|
||||||
|
Заказан оркестратором отдельно от дофикса по канону, ПОСЛЕ его закрытия — чтобы не смешивать предметы
|
||||||
|
посреди лендинга. Ряд регистра **`PD-454`**.
|
||||||
|
|
||||||
|
**ЧТО БЫЛО.** Каждая строка, которую `make check` печатает о прогоне — список пакетов, ряды `ALARM`,
|
||||||
|
список падений, счёт скипов, — есть ГРЕП по одному файлу. Греп отвечает на ОТСУТСТВУЮЩИЙ файл ровно
|
||||||
|
тем же, чем на чистый: ничем. Поэтому рецепт доходил до финального `else` и печатал
|
||||||
|
«every test ran: no host condition was missing» о прогоне, чей лог исчез. **Наблюдено, а не выведено:**
|
||||||
|
три подряд `grep: .check.log: No such file or directory`, следом эта самая строка — на прогоне, где
|
||||||
|
скипов было ПЯТЬ. Код возврата 0, потому что статус берётся от `go test` ДО грепов.
|
||||||
|
|
||||||
|
**ПРИЧИНА.** Фиксированное имя лога, общее для всех прогонов в каталоге. Два прогона в одном каталоге
|
||||||
|
здесь НОРМА — батарею гоняют и зона, и оркестратор, — и первый закончивший удаляет улику второго
|
||||||
|
посреди рецепта.
|
||||||
|
|
||||||
|
**ЛЕЧЕНИЕ ДВУМЯ ПОЛОВИНАМИ, и они не дублируют друг друга.** Имя лога стало ПОПРОГОННЫМ (`$$` — PID
|
||||||
|
шелла): снимает сегодняшнюю ПРИЧИНУ. Гвард «нет лога ⇒ выйти красным ДО первого чтения»: снимает
|
||||||
|
КЛАСС — отказавший редирект, полный диск, рука. ⚠ Проверено ИСПОЛНЕНИЕМ, а не рассуждением: подменённый
|
||||||
|
`GO`, который не пишет лога, даёт красный выход и две строки объяснения вместо справки.
|
||||||
|
|
||||||
|
**ГЕЙТ ДЕРЖИТ ОДНУ БЕЗОПАСНУЮ ФОРМУ И ГОВОРИТ ЭТО ПРЯМО** —
|
||||||
|
`gates.TestTheBatteryCannotReportCleanlinessWithoutItsLog`: проверка лога, затем выход с ЛИТЕРАЛЬНЫМ
|
||||||
|
ненулевым кодом, затем чтения. Внутри формы детали свободны (имя попрогонное или нет, `-s`/`-f`/`-e`),
|
||||||
|
а иная — пусть и верная — форма покраснеет НАМЕРЕННО: пере-кроить единственный рецепт, через который
|
||||||
|
отчитывается вся батарея, должен человек и пере-пиньнуть, а не проскочить молча. Имя лога гейт читает
|
||||||
|
ИЗ РЕЦЕПТА (из редиректа), чтобы переименование не заставило его смотреть на другой файл и промолчать.
|
||||||
|
**ПЯТЬ мутаций ловятся, все проверены исполнением:** гвард удалён · гвард после первого чтения · тест
|
||||||
|
есть, `exit` убран · `exit 1` → `exit 0` · `exit` без кода.
|
||||||
|
|
||||||
|
⛔ **ВТОРАЯ РЕДАКЦИЯ ГЕЙТА, И НАШЛА ЕЁ НЕ Я, А ПРИЁМКА.** Первая требовала лишь, чтобы после гварда был
|
||||||
|
шаг, начинающийся с `exit`, — и `exit 0` проходил ЗЕЛЁНЫМ. Оркестратор замерил исполнением: с `exit 0`
|
||||||
|
и подменённым `GO` батарея печатает «THE BATTERY LEFT NO LOG» и **возвращает `MAKE-EXIT = 0`**.
|
||||||
|
Воспроизведено мной, файл восстановлен по sha256. ⇒ **тот же дефект, переодетый, и в одном отношении
|
||||||
|
ХУЖЕ исходного:** прежняя ложная справка была СТРОКОЙ, которую человек ловит глазами, эта — КОД
|
||||||
|
ВОЗВРАТА, который потребляет машина: CI, скрипт, лендинг. ⚠ И моё собственное сообщение об ошибке
|
||||||
|
обещало «so this exits RED», проверяя лишь НАЛИЧИЕ выхода, — та же болезнь, что гейт лечит, внутри
|
||||||
|
него самого; формулировка подтянута под проверяемое. Теперь требуется ЛИТЕРАЛ: голый `exit` несёт
|
||||||
|
статус предыдущей команды (`echo`, то есть ноль), а `exit $var` из рецепта не судится вовсе — оба
|
||||||
|
отвергаются, а не угадываются. Сквозная проверка после правки: `MAKE-EXIT=2`, громкая строка
|
||||||
|
напечатана один раз, ложная справка — ноль раз.
|
||||||
|
|
||||||
|
⚠ **И ГЕЙТ ПОЙМАЛ САМ СЕБЯ.** Его первая редакция считала ЧТЕНИЕМ слово `grep` внутри объясняющего
|
||||||
|
`echo` того самого гварда — и объявила гвард опоздавшим. Это ровно тот substring-vs-invocation капкан,
|
||||||
|
о котором соседний гейт в этом же файле пишет отдельным абзацем; я в него вошла, цитируя его.
|
||||||
|
Поймано прогоном на второй минуте, не рассуждением. ⇒ различается ВЫЗОВ, а не слово.
|
||||||
|
|
||||||
|
⚠ **Заодно снят второй носитель одной грамматики:** разбор рецепта Makefile был написан внутри
|
||||||
|
соседнего гейта, и вторая копия его же в новом тесте была бы ровно тем «вторым носителем, свободным
|
||||||
|
разойтись», который этот пак вычищал из денежного пути. Вынесен в `makeRecipe(t, target)`, оба гейта
|
||||||
|
читают один разбор; прежний тест зелен без правок.
|
||||||
|
|
||||||
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
|
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
|
||||||
|
|
||||||
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
|
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
|
||||||
|
|
@ -731,10 +783,15 @@ ordered_units: integer|null // НОВОЕ: сколько юнитов ку
|
||||||
TM_PLATFORM_TEST_PGDUMP/PGRESTORE · достижимый пользовательский systemd
|
TM_PLATFORM_TEST_PGDUMP/PGRESTORE · достижимый пользовательский systemd
|
||||||
load average 5.3 перед стартом — режим, в котором время не врёт (D39.197 п.5)
|
load average 5.3 перед стартом — режим, в котором время не врёт (D39.197 п.5)
|
||||||
|
|
||||||
make check → MAKE-EXIT=2
|
make check → MAKE-EXIT=0 ⟵ ПЕРЕ-СНЯТО 06.09 ПОСЛЕ ДОФИКСА ПО КАНОНУ, красных НЕТ
|
||||||
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
|
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
|
||||||
ПАКЕТОВ 21: ok 20, FAIL 1
|
ПАКЕТОВ 20: ok 20, FAIL 0
|
||||||
ТЕСТОВ верхнеуровневых 850: PASS 844 · FAIL 1 · SKIP 5 (плюс 252 подтеста)
|
ТЕСТОВ верхнеуровневых 852: PASS 847 · FAIL 0 · SKIP 5
|
||||||
|
(847-й — новый гейт прибора, `TestTheBatteryCannotReportCleanlinessWithoutItsLog`)
|
||||||
|
⚠ ИСПР.: прежняя редакция этого блока писала «ПАКЕТОВ 21». Их ДВАДЦАТЬ. Двадцать первым
|
||||||
|
я посчитала лишнюю строку `FAIL` в выводе `make` — он печатает и падение ПАКЕТА, и
|
||||||
|
итоговое голое `FAIL`. Ошибка ровно того же рода, что ловил четвёртый заход: величина
|
||||||
|
взята из вывода прибора, не разобрав, что прибор печатает.
|
||||||
⚠ счёт снят из `.check.log` (`make check` гонит `-v`); `go test` БЕЗ `-v` строк
|
⚠ счёт снят из `.check.log` (`make check` гонит `-v`); `go test` БЕЗ `-v` строк
|
||||||
`--- SKIP` не печатает вовсе, и греп по нему даёт ЛОЖНЫЙ НОЛЬ — этой ошибкой
|
`--- SKIP` не печатает вовсе, и греп по нему даёт ЛОЖНЫЙ НОЛЬ — этой ошибкой
|
||||||
четвёртый заход уже был пойман
|
четвёртый заход уже был пойман
|
||||||
|
|
@ -742,12 +799,25 @@ ALARM PD-count: 12 (baseline 12) — ПЕРЕ-СНЯТА базой: PD-375 и P
|
||||||
по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ») и
|
по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ») и
|
||||||
прецеденту PD-168; было 14 (baseline 14) с двумя объявленными исключениями
|
прецеденту PD-168; было 14 (baseline 14) с двумя объявленными исключениями
|
||||||
|
|
||||||
КРАСНЫЙ ОДИН, И ОН РАТИФИЦИРОВАН:
|
КРАСНЫХ НЕТ. Единственный красный этого пака — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
|
||||||
internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified
|
(«announces 0.12.0, canon is 0.11.0») — ПОГАШЕН оркестратором вторым актом: канон на диске
|
||||||
«this build announces contract 0.12.0 and the ratified canon is 0.11.0»
|
0.12.0, пара сомкнулась. Порядок был ратифицирован — код первым, канон вторым (эррата 04.09-в:
|
||||||
Истинная причина: пара «код + канон» не закрыта по НОВОМУ минору. Порядок ратифицирован —
|
обратный порядок стоил суток лжи на проводе), и он сработал как задумано.
|
||||||
код первым, канон вторым (эррата 04.09-в: обратный порядок стоил суток лжи на проводе).
|
|
||||||
Гасит его канон, а не подгонка константы.
|
⛔ ДЕФЕКТ САМОГО ПРИБОРА, найденный этим же прогоном и НЕ ПОЧИНЕННЫЙ (передан оркестратору):
|
||||||
|
`make check` пишет лог в ФИКСИРОВАННЫЙ путь `platform/.check.log` и удаляет его при зелени. Два
|
||||||
|
прогона в одном каталоге — а сегодня они шли — молча уничтожают улику друг друга, и провал этот
|
||||||
|
НЕ ГРОМКИЙ: греп по `--- SKIP` не находит файла, и Makefile печатает
|
||||||
|
«--- every test ran: no host condition was missing ---». **Это ЛОЖНАЯ ЧИСТАЯ СПРАВКА:** скипы были
|
||||||
|
(пять), а строка сказала, что не было ни одного. Наблюдено 06.09 в прогоне v16 — три подряд
|
||||||
|
`grep: .check.log: No such file or directory`, и следом чистая справка. ⇒ числа ниже сняты НЕ из
|
||||||
|
`make check`, а собственным `go test ./... -race -count=1 -v` в файл вне каталога зоны, которого
|
||||||
|
чужой прогон не достанет. Починка мелкая (падать громко, если лога нет, либо уникальное имя), но
|
||||||
|
Makefile читает гейт `gates.TestTheBatteryNamesEveryHostConditionItsTestsRead`, и правку прибора
|
||||||
|
посреди чужого лендинга зона в свой дофикс НЕ внесла. ✅ **ПОЧИНЕНО ОТДЕЛЬНЫМ АКТОМ** после закрытия
|
||||||
|
лендинга, по заказу оркестратора — секция «ПРИБОР ПЕРЕСТАЛ УМЕТЬ…» выше, ряд `PD-454`. Числа ниже
|
||||||
|
сняты УЖЕ ПОСЛЕ этой починки, и `make check` в том же прогоне напечатал честное
|
||||||
|
«did NOT run: 5 skipped» вместо прежней чистой справки.
|
||||||
|
|
||||||
СКИПОВ ПЯТЬ, ВСЕ НАЗВАНЫ, ВСЕ ОДНОЙ ПРИЧИНЫ — артефакта контраста (`mining-contrast.zh.txt`) нет на
|
СКИПОВ ПЯТЬ, ВСЕ НАЗВАНЫ, ВСЕ ОДНОЙ ПРИЧИНЫ — артефакта контраста (`mining-contrast.zh.txt`) нет на
|
||||||
этом хосте ни под одним путём: он многомегабайтный, поставляется деплоем и намеренно не в git.
|
этом хосте ни под одним путём: он многомегабайтный, поставляется деплоем и намеренно не в git.
|
||||||
|
|
|
||||||
|
|
@ -134,57 +134,10 @@ func TestTheBatteryNamesEveryHostConditionItsTestsRead(t *testing.T) {
|
||||||
// vanishes from the hint and BOTH sides fall silent together — the walk would not see it either if
|
// vanishes from the hint and BOTH sides fall silent together — the walk would not see it either if
|
||||||
// the walk were narrowed to match. So the recipe's own anchors are read here and held to the ones
|
// the walk were narrowed to match. So the recipe's own anchors are read here and held to the ones
|
||||||
// this gate understands; narrowing either is what goes red, before any test uses the idiom.
|
// this gate understands; narrowing either is what goes red, before any test uses the idiom.
|
||||||
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
|
|
||||||
// tab-indented — and not at the first blank line: a recipe split into two indented lines with a
|
|
||||||
// blank between them is legal make with identical output, and a gate that failed on it would send
|
|
||||||
// the next person who tidies this file looking for a defect that is not there.
|
|
||||||
lines := strings.Split(string(recipe), "\n")
|
|
||||||
start := -1
|
|
||||||
for i, l := range lines {
|
|
||||||
if strings.HasPrefix(l, "conditions:") {
|
|
||||||
start = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if start < 0 {
|
|
||||||
t.Fatal("the Makefile declares no `conditions` target: the battery's hint has no source")
|
|
||||||
}
|
|
||||||
var recipeLines []string
|
|
||||||
for _, l := range lines[start+1:] {
|
|
||||||
if l != "" && !strings.HasPrefix(l, "\t") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
recipeLines = append(recipeLines, l)
|
|
||||||
}
|
|
||||||
// The GREP EXPRESSIONS the recipe runs, not words that appear near them: an idiom named in a
|
// The GREP EXPRESSIONS the recipe runs, not words that appear near them: an idiom named in a
|
||||||
// comment inside the recipe would satisfy a substring test while the grep beside it had been
|
// comment inside the recipe would satisfy a substring test while the grep beside it had been
|
||||||
// narrowed back, which is the hole this check exists to close.
|
// narrowed back, which is the hole this check exists to close.
|
||||||
//
|
commands := makeRecipe(t, "conditions")
|
||||||
// LOGICAL commands, joined the way the shell joins them: a pipeline split across a backslash
|
|
||||||
// continuation is one command however many lines it occupies, and a check that judged physical
|
|
||||||
// lines would call a legal reformat a defect — the thing the slice above is careful not to do.
|
|
||||||
commands := []string{}
|
|
||||||
pending := ""
|
|
||||||
for _, l := range recipeLines {
|
|
||||||
code := strings.TrimPrefix(l, "\t")
|
|
||||||
if head, _, found := strings.Cut(code, "#"); found {
|
|
||||||
code = head
|
|
||||||
}
|
|
||||||
trimmed := strings.TrimRight(code, " \t")
|
|
||||||
if strings.HasSuffix(trimmed, `\`) {
|
|
||||||
pending += strings.TrimSuffix(trimmed, `\`) + " "
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
commands = append(commands, pending+code)
|
|
||||||
pending = ""
|
|
||||||
}
|
|
||||||
if pending != "" {
|
|
||||||
commands = append(commands, pending)
|
|
||||||
}
|
|
||||||
// EVERY grep that looks for an environment condition must carry BOTH idioms, and the recipe runs
|
// EVERY grep that looks for an environment condition must carry BOTH idioms, and the recipe runs
|
||||||
// more than one of them: the names come from one grep and the packages that read each name from
|
// more than one of them: the names come from one grep and the packages that read each name from
|
||||||
// another. An idiom dropped from either is a condition or an attribution that quietly goes
|
// another. An idiom dropped from either is a condition or an attribution that quietly goes
|
||||||
|
|
@ -359,3 +312,204 @@ func equal(got, want []string) bool {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeRecipe is the Makefile's recipe for one target, as the SHELL would see it: one string per
|
||||||
|
// logical command, comments cut, backslash continuations joined.
|
||||||
|
//
|
||||||
|
// It lives in one place because two gates read recipes now, and two hand-written parsers of one
|
||||||
|
// grammar are a second carrier free to drift from the first — the defect this zone spent a day
|
||||||
|
// removing from its own money path.
|
||||||
|
//
|
||||||
|
// ⚠ The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
|
||||||
|
// tab-indented — and NOT at the first blank line: a recipe split into two indented lines with a blank
|
||||||
|
// between them is legal make with identical output, and a gate that failed on it would send the next
|
||||||
|
// person who tidies this file looking for a defect that is not there. ⚠ And the joining is what makes
|
||||||
|
// the unit LOGICAL: a pipeline split across a continuation is one command however many lines it
|
||||||
|
// occupies, so a legal reformat is not a finding.
|
||||||
|
func makeRecipe(t *testing.T, target string) []string {
|
||||||
|
t.Helper()
|
||||||
|
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(recipe), "\n")
|
||||||
|
start := -1
|
||||||
|
for i, l := range lines {
|
||||||
|
if strings.HasPrefix(l, target+":") {
|
||||||
|
start = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
t.Fatalf("the Makefile declares no `%s` target: the gate that reads it has no source", target)
|
||||||
|
}
|
||||||
|
var recipeLines []string
|
||||||
|
for _, l := range lines[start+1:] {
|
||||||
|
if l != "" && !strings.HasPrefix(l, "\t") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
recipeLines = append(recipeLines, l)
|
||||||
|
}
|
||||||
|
commands := []string{}
|
||||||
|
pending := ""
|
||||||
|
for _, l := range recipeLines {
|
||||||
|
code := strings.TrimPrefix(l, "\t")
|
||||||
|
if head, _, found := strings.Cut(code, "#"); found {
|
||||||
|
code = head
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimRight(code, " \t")
|
||||||
|
if strings.HasSuffix(trimmed, `\`) {
|
||||||
|
pending += strings.TrimSuffix(trimmed, `\`) + " "
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
commands = append(commands, pending+code)
|
||||||
|
pending = ""
|
||||||
|
}
|
||||||
|
if pending != "" {
|
||||||
|
commands = append(commands, pending)
|
||||||
|
}
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⛔ THE BATTERY MUST NOT BE ABLE TO REPORT CLEANLINESS OVER A LOG IT DID NOT READ.
|
||||||
|
//
|
||||||
|
// Every line `make check` prints about the run — the package list, the ALARM rows, the failure list,
|
||||||
|
// the skip count — is a GREP over one file. Grep answers a MISSING file exactly as it answers a clean
|
||||||
|
// one: with nothing. So the recipe could reach its final `else` and print «every test ran: no host
|
||||||
|
// condition was missing» about a run whose log had vanished, which is a bill of health for a
|
||||||
|
// measurement that never happened.
|
||||||
|
//
|
||||||
|
// It is not hypothetical and it is not rare: observed 06.09 on a run with FIVE skips, three
|
||||||
|
// `grep: .check.log: No such file or directory` followed by that very line. The cause was a FIXED log
|
||||||
|
// name shared by every run in the directory — and two batteries in one directory is the normal case
|
||||||
|
// here, the zone and the orchestrator both run one.
|
||||||
|
//
|
||||||
|
// So the recipe must TEST the log before it reads it and leave RED when it is not there. This gate
|
||||||
|
// holds ONE SAFE SHAPE and says so plainly rather than pretending to prove the property: a test of the
|
||||||
|
// log, then an exit with a LITERAL NON-ZERO code, then the reads. Within that shape the details are
|
||||||
|
// free — the log may be named per-run or not, the guard may be `-s`, `-f` or `-e`. A different but
|
||||||
|
// equally correct shape (say, all the reads nested inside `if [ -s log ]; then … else exit 1; fi`)
|
||||||
|
// will go red here, and that is deliberate: re-shaping the one recipe the whole battery reports
|
||||||
|
// through should be a decision somebody takes and re-pins, not a silent pass.
|
||||||
|
//
|
||||||
|
// Mutation caught: deleting the guard; moving it after the first grep; dropping the `exit` so the
|
||||||
|
// recipe notices and carries on; and — the one the first edition missed — `exit 0`, which satisfies
|
||||||
|
// "there is an exit" while returning SUCCESS from a run whose evidence is gone.
|
||||||
|
func TestTheBatteryCannotReportCleanlinessWithoutItsLog(t *testing.T) {
|
||||||
|
steps := []string{}
|
||||||
|
for _, cmd := range makeRecipe(t, "check") {
|
||||||
|
for _, s := range strings.Split(cmd, ";") {
|
||||||
|
steps = append(steps, strings.TrimSpace(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WHERE THE LOG COMES FROM, read out of the recipe rather than assumed: the redirect of the test
|
||||||
|
// run names it, so a rename cannot make this gate look at the wrong file and pass.
|
||||||
|
logRef := ""
|
||||||
|
for _, s := range steps {
|
||||||
|
if !strings.Contains(s, "test ./...") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, after, found := strings.Cut(s, "> "); found {
|
||||||
|
logRef = strings.Trim(strings.Fields(after)[0], `"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if logRef == "" {
|
||||||
|
t.Fatal("the `check` recipe does not redirect its test run to a file this gate can find: " +
|
||||||
|
"either the battery stopped keeping a log, or its shape moved and this gate now reads nothing")
|
||||||
|
}
|
||||||
|
// One shell command per step, with the leading keyword taken off, so that `then exit 1` is read as
|
||||||
|
// `exit 1` and `then echo …` as an echo. Done once here because every check below asks the same
|
||||||
|
// question of the same string, and two strippers would drift.
|
||||||
|
cmds := make([]string, len(steps))
|
||||||
|
for i, s := range steps {
|
||||||
|
cmd := strings.TrimSpace(s)
|
||||||
|
for {
|
||||||
|
stripped := cmd
|
||||||
|
for _, kw := range []string{"then", "else", "do", "{"} {
|
||||||
|
if rest, found := strings.CutPrefix(stripped, kw); found && strings.TrimLeft(rest, " \t") != rest {
|
||||||
|
stripped = strings.TrimLeft(rest, " \t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stripped == cmd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cmd = stripped
|
||||||
|
}
|
||||||
|
cmds[i] = cmd
|
||||||
|
}
|
||||||
|
reads, guard, exits := -1, -1, -1
|
||||||
|
exitCode := ""
|
||||||
|
for i, cmd := range cmds {
|
||||||
|
if !strings.Contains(cmd, logRef) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// A grep INVOCATION, not the word: the recipe's own guard EXPLAINS itself in an `echo`, and the
|
||||||
|
// explanation says «grep». The first edition of this gate counted that sentence as a read and
|
||||||
|
// reported the guard as too late — which is the same substring-versus-invocation trap the
|
||||||
|
// sibling check above spends a paragraph on, walked into by the test that quotes it.
|
||||||
|
greps := strings.Contains(cmd, "$$(grep") ||
|
||||||
|
(!strings.HasPrefix(cmd, "echo") && strings.Contains(cmd, "grep"))
|
||||||
|
switch {
|
||||||
|
case greps && reads < 0:
|
||||||
|
reads = i
|
||||||
|
case strings.HasPrefix(cmd, "if [") || strings.HasPrefix(cmd, "[ "):
|
||||||
|
if guard < 0 && (strings.Contains(cmd, "-s ") || strings.Contains(cmd, "-f ") || strings.Contains(cmd, "-e ")) {
|
||||||
|
guard = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ⛔ THE EXIT'S CODE, not merely its presence — and this half was MISSING from the first edition.
|
||||||
|
// `exit 0` after the guard satisfied "there is an exit" while leaving the recipe to announce that
|
||||||
|
// the evidence is gone and then RETURN SUCCESS. That is the very defect this gate exists for,
|
||||||
|
// wearing different clothes and worse in one respect: the old false clean bill was a LINE a person
|
||||||
|
// could catch, this one is the EXIT CODE, which is what CI and the landing read. Measured, not
|
||||||
|
// argued: with `exit 1` mutated to `exit 0` this gate stayed green and `make check` returned 0
|
||||||
|
// while printing «THE BATTERY LEFT NO LOG».
|
||||||
|
//
|
||||||
|
// A literal is required. `exit` bare carries the previous command's status — here the status of an
|
||||||
|
// `echo`, which is zero — and `exit $var` cannot be judged from the recipe at all; both are refused
|
||||||
|
// rather than guessed, because a guard whose code is not readable here is a guard this gate cannot
|
||||||
|
// promise anything about.
|
||||||
|
for i, cmd := range cmds {
|
||||||
|
if guard < 0 || i <= guard {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rest, found := strings.CutPrefix(cmd, "exit"); found {
|
||||||
|
exits, exitCode = i, strings.TrimSpace(rest)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reads < 0 {
|
||||||
|
t.Fatalf("no step of the `check` recipe greps %s: the gate is reading the wrong target or the "+
|
||||||
|
"battery no longer reports what the log holds", logRef)
|
||||||
|
}
|
||||||
|
if guard < 0 {
|
||||||
|
t.Fatalf("the `check` recipe reads %s at step %d and never TESTS that it is there. A missing log "+
|
||||||
|
"is indistinguishable from a clean one to grep, so the recipe will print «every test ran» "+
|
||||||
|
"over a run it did not measure — the defect this gate exists for", logRef, reads)
|
||||||
|
}
|
||||||
|
if guard > reads {
|
||||||
|
t.Errorf("the `check` recipe tests %s at step %d but has already read it at step %d: the first "+
|
||||||
|
"read is where the false clean bill starts, so the guard must come before it", logRef, guard, reads)
|
||||||
|
}
|
||||||
|
if exits < 0 || exits > reads {
|
||||||
|
t.Errorf("the `check` recipe tests %s at step %d but does not exit before its first read at step "+
|
||||||
|
"%d: noticing the absence and carrying on prints the same bill of health as never looking",
|
||||||
|
logRef, guard, reads)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code, err := strconv.Atoi(exitCode)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("the guard of the `check` recipe exits with %q, which this gate cannot read as a number: "+
|
||||||
|
"a bare `exit` carries the previous command's status — an `echo`, so zero — and a variable "+
|
||||||
|
"cannot be judged from the recipe. Write a literal, so that what the recipe promises is what "+
|
||||||
|
"a reader can check", "exit "+exitCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if code == 0 {
|
||||||
|
t.Errorf("the guard of the `check` recipe says the log is missing and then exits %d, which is "+
|
||||||
|
"SUCCESS: the battery announces that its evidence is gone and reports a green run. That is "+
|
||||||
|
"the same false clean bill this gate exists for, moved from a LINE a person reads into the "+
|
||||||
|
"EXIT CODE a machine reads — and the landing reads the code", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue