Files
av b6b0976c19 исправлены находки ревью проектной стороны
- манифест читается так, как записан: решётка внутри строки не открывает
  комментарий, скобка внутри комментария не закрывает массив, имя внутри
  комментария не становится подпиской; новый ключ встаёт после массива,
  а не внутрь него
- всё записываемое проходит через manifest.Quote — обратный слэш в пути
  делал файл, который инструмент сам не читает
- маркер локальной части переехал в doc и пропускает огороженные блоки:
  процитированный в примере маркер больше не считается границей, а копия
  без маркера не перезаписывается молча
- лишний позиционный аргумент отсекается: flag прекращал разбор и прятал
  флаги после себя, из-за чего pull, list и check игнорировали --for
- заведены тесты проверок копий, включая молчание на исправной копии
2026-07-27 21:16:29 +03:00

178 lines
6.1 KiB
Go

package check_test
import (
"strings"
"testing"
"git.vakhrushev.me/av/convy/internal/check"
"git.vakhrushev.me/av/convy/internal/doc"
)
// A copy is checked without a suite next to it: no manifest, no prefix of its
// own, no path back to where it came from. These fixtures are therefore written
// out whole rather than assembled — what the check sees is what a consuming
// repository holds.
const copyVersionLine = `Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.`
// soundCopy is what the assembler writes: two layers, the second a section of
// the first, a local part with a rule of the repository in it.
const soundCopy = `---
origin: time
---
# Время
Как приложение записывает моменты.
` + copyVersionLine + `
## Правила
### TIME-1. Момент записывается в UTC
**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.
**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.
### TIME-2. Ширина строки фиксируется
**СНЯТО 2026-07-27.** ширина следует из TIME-1.
## Время: реализация на Go
Как базовый слой выполняется в Go-коде.
### Правила
#### GTIM-1. «Сейчас» берётся у слоя хранилища
**ДОЛЖЕН.** Текущее время приходит из ` + "`store.Now()`" + `.
**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.
<!-- conv:local -->
TIME-1 — МЕХАНИЗИРОВАНО: ` + "`internal/archrules`" + `.
Ссылка на чужую тему: SLOG-4 — эта копия её не держит.
### XTIM-1. Часы в тестах замораживаются
**ДОЛЖЕН.** Тест берёт время у подменённого ` + "`store.Now`" + `.
**ПОЧЕМУ.** Плавающее время делает падение теста невоспроизводимым.
`
func checkCopy(t *testing.T, body string) *check.Report {
t.Helper()
d, err := doc.Parse("docs/conventions/time.md", body)
if err != nil {
t.Fatalf("parsing the fixture: %v", err)
}
return check.Copies([]*doc.Document{d})
}
// The one that matters most: a check that reddens on a sound file is a check
// that gets switched off whole.
func TestCopiesAreSilentOnASoundCopy(t *testing.T) {
rep := checkCopy(t, soundCopy)
if len(rep.Findings()) == 0 {
return
}
var b strings.Builder
for _, f := range rep.Findings() {
b.WriteString(" " + f.Msg + "\n")
}
t.Errorf("a sound copy produced findings:\n%s", b.String())
}
// A convention about keeping copies quotes the marker in an example. The
// quotation is markup shown, not markup meant.
func TestCopiesReadNoMarkerInsideAFencedBlock(t *testing.T) {
quoting := strings.Replace(soundCopy, "## Время: реализация на Go",
"## Пример\n\n"+"```markdown\n<!-- conv:local -->\n```\n\n## Время: реализация на Go", 1)
rep := checkCopy(t, quoting)
for _, f := range rep.Findings() {
if strings.Contains(f.Msg, "marker") {
t.Errorf("a quoted marker was taken for the boundary: %s", f.Msg)
}
}
}
func TestCopiesCatchWhatOnlyACopyCanGetWrong(t *testing.T) {
cases := []struct {
name string
edit func(string) string
want string
}{{
name: "no origin key",
edit: func(s string) string { return strings.Replace(s, "origin: time", "topic: time", 1) },
want: "no origin key",
}, {
name: "a key of a suite file left in the front matter",
edit: func(s string) string {
return strings.Replace(s, "origin: time", "origin: time\nextends: arch/time.md", 1)
},
want: "key \"extends\" of a suite file",
}, {
name: "no marker at all",
edit: func(s string) string { return strings.Replace(s, "<!-- conv:local -->", "", 1) },
want: "carries no <!-- conv:local --> marker",
}, {
name: "a second marker",
edit: func(s string) string { return s + "\n<!-- conv:local -->\n" },
want: "second <!-- conv:local --> marker",
}, {
name: "a rule of the repository above the marker",
edit: func(s string) string {
return strings.Replace(s, "### TIME-2.", "### XTIM-9. Своё правило\n\n**ДОЛЖЕН.** Своя норма.\n\n**ПОЧЕМУ.** Своя причина.\n\n### TIME-2.", 1)
},
want: "would wipe it",
}, {
name: "a rule of the suite below the marker",
edit: func(s string) string { return strings.Replace(s, "### XTIM-1.", "### ZTIM-1.", 1) },
want: "takes a prefix the suite could hand out",
}, {
name: "a gap in the numbering of one of the prefixes",
edit: func(s string) string { return strings.Replace(s, "#### GTIM-1.", "#### GTIM-2.", 1) },
want: "numbering is not contiguous",
}, {
name: "a reference to a rule the file holds no such number of",
edit: func(s string) string {
return strings.Replace(s, "TIME-1 — МЕХАНИЗИРОВАНО", "TIME-9 — МЕХАНИЗИРОВАНО", 1)
},
want: "points at a rule this file does not hold",
}, {
name: "no language version line",
edit: func(s string) string {
return strings.Replace(s, copyVersionLine, "Просто вступление.", 1)
},
want: "no language version line",
}, {
name: "a word of another vocabulary",
edit: func(s string) string {
return strings.Replace(s, "**ДОЛЖЕН.** Момент", "**MUST.** Момент", 1)
},
want: "belongs to the \"en\" vocabulary",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rep := checkCopy(t, tc.edit(soundCopy))
for _, f := range rep.Findings() {
if strings.Contains(f.Msg, tc.want) {
return
}
}
var b strings.Builder
for _, f := range rep.Findings() {
b.WriteString(" " + f.Msg + "\n")
}
t.Errorf("nothing said %q; the findings were:\n%s", tc.want, b.String())
})
}
}