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. 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\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, "", "", 1) }, want: "carries no marker", }, { name: "a second marker", edit: func(s string) string { return s + "\n\n" }, want: "second 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()) }) } }