- suite rule дописывает правило: номер берётся следующим за наибольшим, блоки раскладываются в порядке норма, ПОЧЕМУ, ПРИМЕРЫ, --after ставит правило рядом с тем, которое оно уточняет - ступень называется категорией (requirement, prohibition, ...), а не словом языка, поэтому вызывающему не нужно знать, на каком языке записан набор - suite list показывает темы со слоями, а с осью — что возьмёт компонент; отбор слоёв вынесен в suite.Assemble, откуда его возьмут проектные команды - слои темы теперь всегда возвращаются базовым вперёд
202 lines
7.6 KiB
Go
202 lines
7.6 KiB
Go
package cli_test
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.vakhrushev.me/av/convy/internal/cli"
|
||
)
|
||
|
||
func TestRuleTakesTheNextNumberAndKeepsTheOrderOfBlocks(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
code, out := run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "TIME", "--title", "Точность носителя фиксируется",
|
||
"--modality", "requirement",
|
||
"--norm", "У колонки БД и у потока логов точность объявлена и не плавает.",
|
||
"--why", "Плавающая точность ломает сортировку выборочно и невоспроизводимо.")
|
||
if code != cli.OK {
|
||
t.Fatalf("adding a rule returned %d: %s", code, out)
|
||
}
|
||
if !strings.Contains(out, "TIME-3 added") {
|
||
t.Errorf("the number is not the next after the highest:\n%s", out)
|
||
}
|
||
|
||
body := read(t, root, "conventions/time.md")
|
||
want := "### TIME-3. Точность носителя фиксируется\n\n**ДОЛЖЕН.** У колонки БД и у потока логов точность объявлена и не плавает.\n\n**ПОЧЕМУ.** Плавающая точность ломает сортировку выборочно и невоспроизводимо."
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the rule is not laid out as the language fixes it:\n%s", body)
|
||
}
|
||
checkClean(t, root)
|
||
}
|
||
|
||
// A number is never reused, so retiring a rule does not free its number: the
|
||
// next one still comes after the highest.
|
||
func TestRuleNumbersPastARetiredOne(t *testing.T) {
|
||
root := retirable(t)
|
||
run(t, root, "", false, "suite", "retire", "--rule", "TIME-2", "--reason", "не нужно", "--date", "2026-07-27")
|
||
|
||
code, out := run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "TIME", "--title", "Третье", "--modality", "recommendation",
|
||
"--norm", "Норма.", "--why", "Причина.")
|
||
if code != cli.OK {
|
||
t.Fatalf("adding a rule returned %d: %s", code, out)
|
||
}
|
||
if !strings.Contains(out, "TIME-3 added") {
|
||
t.Errorf("the number of a retired rule was reused:\n%s", out)
|
||
}
|
||
checkClean(t, root)
|
||
}
|
||
|
||
// Order in a file goes by reading rather than by number, so a rule elaborating
|
||
// another has to be placeable next to it.
|
||
func TestRulePlacesAfterTheOneItElaborates(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "TIME", "--title", "Третье", "--modality", "recommendation",
|
||
"--norm", "Норма третьего.", "--why", "Причина третьего.")
|
||
code, out := run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "TIME", "--title", "Уточнение первого", "--modality", "permission",
|
||
"--norm", "Норма четвёртого.", "--why", "Причина четвёртого.",
|
||
"--after", "TIME-1")
|
||
if code != cli.OK {
|
||
t.Fatalf("adding a rule returned %d: %s", code, out)
|
||
}
|
||
|
||
body := read(t, root, "conventions/time.md")
|
||
order := []string{"### TIME-1.", "### TIME-4.", "### TIME-2.", "### TIME-3."}
|
||
at := 0
|
||
for _, id := range order {
|
||
i := strings.Index(body[at:], id)
|
||
if i < 0 {
|
||
t.Fatalf("%s is missing or out of place:\n%s", id, body)
|
||
}
|
||
at += i
|
||
}
|
||
checkClean(t, root)
|
||
}
|
||
|
||
// The step is named by its category rather than by a word of any one language,
|
||
// so that a caller need not know which language the suite is written in. The
|
||
// word itself is taken too, for whoever has it at hand.
|
||
func TestRuleTakesTheStepByCategoryOrByWord(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "SLOG", "--title", "Через категорию", "--modality", "not-recommended",
|
||
"--norm", "Норма.", "--why", "Причина.")
|
||
run(t, root, "", false, "suite", "rule",
|
||
"--prefix", "SLOG", "--title", "Через слово", "--modality", "ДОПУСКАЕТСЯ",
|
||
"--norm", "Норма.", "--why", "Причина.")
|
||
|
||
body := read(t, root, "conventions/logging.md")
|
||
for _, want := range []string{"**НЕ СЛЕДУЕТ.** Норма.", "**ДОПУСКАЕТСЯ.** Норма."} {
|
||
if !strings.Contains(body, want) {
|
||
t.Errorf("the step did not render as %q:\n%s", want, body)
|
||
}
|
||
}
|
||
checkClean(t, root)
|
||
}
|
||
|
||
func TestRuleRefusesWhatItCannotDo(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
cases := []struct {
|
||
name string
|
||
args []string
|
||
want string
|
||
}{{
|
||
name: "a prefix the suite does not declare",
|
||
args: []string{"--prefix", "ZZZZ", "--title", "Т", "--modality", "requirement", "--norm", "Н", "--why", "П"},
|
||
want: "no live prefix ZZZZ",
|
||
}, {
|
||
name: "a step that is not on the scale",
|
||
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "maybe", "--norm", "Н", "--why", "П"},
|
||
want: "one of: requirement, prohibition",
|
||
}, {
|
||
name: "placing after a rule of another file",
|
||
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "requirement", "--norm", "Н", "--why", "П", "--after", "SLOG-1"},
|
||
want: "belongs to another file",
|
||
}, {
|
||
name: "a rationale left out",
|
||
args: []string{"--prefix", "TIME", "--title", "Т", "--modality", "requirement", "--norm", "Н"},
|
||
want: "--why",
|
||
}}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
code, out := run(t, root, "", false, append([]string{"suite", "rule"}, tc.args...)...)
|
||
if code == cli.OK {
|
||
t.Fatalf("the command went through:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, tc.want) {
|
||
t.Errorf("the refusal does not say %q:\n%s", tc.want, out)
|
||
}
|
||
})
|
||
}
|
||
checkClean(t, root)
|
||
}
|
||
|
||
func TestListShowsTheSuiteBaseLayerFirst(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
code, out := run(t, root, "", false, "suite", "list")
|
||
if code != cli.OK {
|
||
t.Fatalf("listing returned %d: %s", code, out)
|
||
}
|
||
for _, want := range []string{"time — время", "logging — логирование", "TIME", "GTIM", "lang=go", "base"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("the listing lacks %q:\n%s", want, out)
|
||
}
|
||
}
|
||
if strings.Index(out, "conventions/time.md") > strings.Index(out, "conventions/lang/go/time.md") {
|
||
t.Errorf("a language layer came before the base one:\n%s", out)
|
||
}
|
||
}
|
||
|
||
// The selection is the read-only half of assembly: a layer travels when its
|
||
// axis keys agree with the component, and the base layer travels always.
|
||
func TestListSelectsWhatAComponentWouldTake(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
code, out := run(t, root, "", false, "suite", "list", "--topic", "time", "--lang", "go")
|
||
if code != cli.OK {
|
||
t.Fatalf("listing returned %d: %s", code, out)
|
||
}
|
||
if strings.Contains(out, "left out") {
|
||
t.Errorf("a component of the right language left a layer out:\n%s", out)
|
||
}
|
||
|
||
code, out = run(t, root, "", false, "suite", "list", "--topic", "time", "--lang", "python")
|
||
if code != cli.OK {
|
||
t.Fatalf("listing returned %d: %s", code, out)
|
||
}
|
||
if !strings.Contains(out, "conventions/lang/go/time.md") || !strings.Contains(out, "left out") {
|
||
t.Errorf("the go layer was not left out for a python component:\n%s", out)
|
||
}
|
||
if !strings.Contains(out, "conventions/time.md") {
|
||
t.Errorf("the base layer did not travel:\n%s", out)
|
||
}
|
||
}
|
||
|
||
func TestListShowsRetiredNames(t *testing.T) {
|
||
root := retirable(t)
|
||
|
||
code, out := run(t, root, "", false, "suite", "list", "--retired")
|
||
if code != cli.OK {
|
||
t.Fatalf("listing returned %d: %s", code, out)
|
||
}
|
||
if !strings.Contains(out, "nothing has been retired yet") {
|
||
t.Errorf("an untouched suite reported retirements:\n%s", out)
|
||
}
|
||
|
||
run(t, root, "", false, "suite", "retire", "--prefix", "SLOG", "--reason", "свёрнута", "--date", "2026-07-27")
|
||
_, out = run(t, root, "", false, "suite", "list", "--retired")
|
||
for _, want := range []string{"SLOG", "2026-07-27", "свёрнута", "ever handed out again"} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("the listing of retired names lacks %q:\n%s", want, out)
|
||
}
|
||
}
|
||
}
|