- манифест читается так, как записан: решётка внутри строки не открывает комментарий, скобка внутри комментария не закрывает массив, имя внутри комментария не становится подпиской; новый ключ встаёт после массива, а не внутрь него - всё записываемое проходит через manifest.Quote — обратный слэш в пути делал файл, который инструмент сам не читает - маркер локальной части переехал в doc и пропускает огороженные блоки: процитированный в примере маркер больше не считается границей, а копия без маркера не перезаписывается молча - лишний позиционный аргумент отсекается: flag прекращал разбор и прятал флаги после себя, из-за чего pull, list и check игнорировали --for - заведены тесты проверок копий, включая молчание на исправной копии
528 lines
19 KiB
Go
528 lines
19 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/check"
|
|
"git.vakhrushev.me/av/convy/internal/cli"
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
)
|
|
|
|
// readingGuide stands in for what a suite puts next to its copies: the short
|
|
// account of the language, naming every word the version line names.
|
|
const readingGuide = `# Как читать конвенцию
|
|
|
|
Слова толкуются так, и только когда написаны заглавными.
|
|
|
|
| Слово | Значение |
|
|
|---|---|
|
|
| ДОЛЖЕН | требование |
|
|
| НЕ ДОЛЖЕН | запрет |
|
|
| СЛЕДУЕТ | рекомендация |
|
|
| НЕ СЛЕДУЕТ | рекомендация против |
|
|
| ДОПУСКАЕТСЯ | разрешение |
|
|
| ПОЧЕМУ | обоснование |
|
|
| ПРИМЕРЫ | иллюстрация |
|
|
| МЕХАНИЗИРОВАНО | чем проверяется |
|
|
| СНЯТО | заглушка снятого правила |
|
|
`
|
|
|
|
// subscribable is a suite a project can take from: the fixture of the retire
|
|
// tests plus the guide that travels next to the copies.
|
|
func subscribable(t *testing.T) string {
|
|
t.Helper()
|
|
root := retirable(t)
|
|
appendRules(t, root, "conventions/lang/go/time.md", `
|
|
### GTIM-1. «Сейчас» берётся у слоя хранилища
|
|
|
|
**ДОЛЖЕН.** Текущее время приходит из `+"`store.Now()`"+`.
|
|
|
|
**ПОЧЕМУ.** Единая точка даёт гарантированный UTC и один формат.
|
|
`)
|
|
|
|
name := filepath.Join(root, "READING.md")
|
|
if err := os.WriteFile(name, []byte(readingGuide), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := read(t, root, "suite.toml")
|
|
body = strings.Replace(body, `# reading = "READING.md"`, `reading = "READING.md"`, 1)
|
|
if err := os.WriteFile(filepath.Join(root, "suite.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkClean(t, root)
|
|
return root
|
|
}
|
|
|
|
// wired builds a project taking from that suite, and returns its root.
|
|
func wired(t *testing.T, suiteRoot string, args ...string) string {
|
|
t.Helper()
|
|
root := filepath.Join(t.TempDir(), "app")
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
all := append([]string{"init", "--source", suiteRoot}, args...)
|
|
if code, out := run(t, root, "", false, all...); code != cli.OK {
|
|
t.Fatalf("convy init returned %d: %s", code, out)
|
|
}
|
|
return root
|
|
}
|
|
|
|
func TestInitAddAndPullBuildACopyOutOfTheLayers(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
code, out := run(t, root, "", false, "add", "time")
|
|
if code != cli.OK {
|
|
t.Fatalf("convy add returned %d: %s", code, out)
|
|
}
|
|
if !strings.Contains(out, "docs/conventions/time.md") {
|
|
t.Errorf("the assembled file is not named:\n%s", out)
|
|
}
|
|
|
|
body := read(t, root, "docs/conventions/time.md")
|
|
for _, want := range []string{
|
|
"origin: time",
|
|
"### TIME-1.",
|
|
"#### GTIM-1.",
|
|
"<!-- conv:local -->",
|
|
} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("the copy lacks %q:\n%s", want, body)
|
|
}
|
|
}
|
|
if strings.Contains(body, "prefix: TIME") {
|
|
t.Errorf("the front matter of a layer travelled into the copy:\n%s", body)
|
|
}
|
|
|
|
// The guide to reading a rule belongs to the suite and travels with the
|
|
// copies; README.md in the same directory belongs to the repository.
|
|
if guide := read(t, root, "docs/conventions/READING.md"); !strings.Contains(guide, "ДОПУСКАЕТСЯ") {
|
|
t.Errorf("the reading guide did not travel:\n%s", guide)
|
|
}
|
|
|
|
if manifest := read(t, root, ".conventions.toml"); !strings.Contains(manifest, `topics = ["time"]`) {
|
|
t.Errorf("the subscription was not written:\n%s", manifest)
|
|
}
|
|
|
|
if code, out := run(t, root, "", false, "check"); code != cli.OK {
|
|
t.Fatalf("checking what the tool assembled returned %d: %s", code, out)
|
|
}
|
|
}
|
|
|
|
// The whole point of the marker: what the repository wrote survives, what the
|
|
// suite wrote is replaced.
|
|
func TestPullKeepsTheLocalPartAndReplacesTheRest(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
name := filepath.Join(root, "docs", "conventions", "time.md")
|
|
body := read(t, root, "docs/conventions/time.md")
|
|
body = strings.Replace(body, "**ДОЛЖЕН.** Момент времени", "**ДОЛЖЕН.** Правка выше маркера", 1)
|
|
body += "\nTIME-1 — МЕХАНИЗИРОВАНО: `internal/archrules`.\n"
|
|
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "pull")
|
|
if code != cli.OK {
|
|
t.Fatalf("convy pull returned %d: %s", code, out)
|
|
}
|
|
if !strings.Contains(out, "local part kept") {
|
|
t.Errorf("the pull did not say the local part survived:\n%s", out)
|
|
}
|
|
|
|
body = read(t, root, "docs/conventions/time.md")
|
|
if strings.Contains(body, "Правка выше маркера") {
|
|
t.Errorf("an edit above the marker survived, and it is declared not to:\n%s", body)
|
|
}
|
|
if !strings.Contains(body, "МЕХАНИЗИРОВАНО: `internal/archrules`") {
|
|
t.Errorf("the local part was lost:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// A layer travels when the axis it declares agrees with the component; the base
|
|
// layer travels always.
|
|
func TestAComponentTakesOnlyTheLayersThatFitIt(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "web", "--dir", "web/docs/conventions", "--lang", "javascript")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
body := read(t, root, "web/docs/conventions/time.md")
|
|
if !strings.Contains(body, "### TIME-1.") {
|
|
t.Errorf("the base layer did not travel:\n%s", body)
|
|
}
|
|
if strings.Contains(body, "GTIM-1") {
|
|
t.Errorf("a go layer travelled into a javascript component:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestProjectCommandsRefuseWhatTheyCannotDo(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{{
|
|
name: "a topic the suite does not declare",
|
|
args: []string{"add", "billing"},
|
|
want: "declares no topic",
|
|
}, {
|
|
name: "a topic taken twice",
|
|
args: []string{"add", "time"},
|
|
want: "subscribed to",
|
|
}, {
|
|
name: "a component that is not there",
|
|
args: []string{"pull", "--for", "mobile"},
|
|
want: "no component",
|
|
}}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
code, out := run(t, root, "", false, 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A file that lost its origin key has become a document of the repository, and
|
|
// assembly has no business overwriting it.
|
|
func TestPullDoesNotOverwriteWhatIsNoLongerACopy(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
name := filepath.Join(root, "docs", "conventions", "time.md")
|
|
body := read(t, root, "docs/conventions/time.md")
|
|
body = strings.Replace(body, "---\norigin: time\n---\n\n", "", 1)
|
|
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "pull")
|
|
if code == cli.OK {
|
|
t.Fatalf("the pull overwrote a document of the repository:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "no origin key") {
|
|
t.Errorf("the refusal does not say why:\n%s", out)
|
|
}
|
|
if got := read(t, root, "docs/conventions/time.md"); strings.Contains(got, "origin: time") {
|
|
t.Errorf("the file was rewritten anyway:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// The context is read off the manifest lying next to you, and nothing is done
|
|
// at a guess.
|
|
func TestAProjectCommandInsideASuiteSaysWhereItIs(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
code, out := run(t, suiteRoot, "", false, "pull")
|
|
if code == cli.OK {
|
|
t.Fatalf("convy pull ran inside a suite:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "convy suite") {
|
|
t.Errorf("the refusal does not point at the commands of a suite:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestListShowsWhatIsTakenAndWhatIsNot(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
code, out := run(t, root, "", false, "list")
|
|
if code != cli.OK {
|
|
t.Fatalf("convy list returned %d: %s", code, out)
|
|
}
|
|
for _, want := range []string{"backend — docs/conventions", "lang=go", "time", "2 layers", "not taken", "logging"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("the listing lacks %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The topic stands in front of the flags, and the flag package stops at the
|
|
// first argument that is not one — so "convy add time --for backend", the form
|
|
// the model writes, has to keep the component rather than drop it in silence.
|
|
func TestAddTakesTheTopicBeforeTheFlags(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "backend/docs", "--lang", "go")
|
|
|
|
body := read(t, root, ".conventions.toml")
|
|
body += "\n[components.web]\ndir = \"web/docs\"\ntopics = []\n"
|
|
if err := os.WriteFile(filepath.Join(root, ".conventions.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "add", "logging", "--for", "web")
|
|
if code != cli.OK {
|
|
t.Fatalf("convy add returned %d: %s", code, out)
|
|
}
|
|
if !strings.Contains(out, "web/docs/logging.md") {
|
|
t.Errorf("the copy did not land in the named component:\n%s", out)
|
|
}
|
|
if manifest := read(t, root, ".conventions.toml"); !strings.Contains(manifest, `topics = ["logging"]`) {
|
|
t.Errorf("the subscription went to the wrong component:\n%s", manifest)
|
|
}
|
|
}
|
|
|
|
// Distinct directories are the only thing that tells two copies of one topic
|
|
// apart, so sharing one is an error of the manifest rather than a reason to
|
|
// rename files.
|
|
func TestTwoComponentsMayNotShareADirectory(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
body := read(t, root, ".conventions.toml")
|
|
body += "\n[components.web]\ndir = \"docs/conventions\"\ntopics = []\n"
|
|
if err := os.WriteFile(filepath.Join(root, ".conventions.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "pull")
|
|
if code == cli.OK {
|
|
t.Fatalf("two components wrote into one directory:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "share the directory") {
|
|
t.Errorf("the refusal does not say what collides:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// Bare, a project command asks; that mode is for a person, and the one with
|
|
// flags is for agents and scripts.
|
|
func TestAddAsksWhichTopicWhenToldNothing(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
code, out := run(t, root, "time\n", true, "add")
|
|
if code != cli.OK {
|
|
t.Fatalf("the dialogue returned %d: %s", code, out)
|
|
}
|
|
if !strings.Contains(out, "Topic") || !strings.Contains(out, "время") {
|
|
t.Errorf("the question carries no hint about what is on offer:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "docs/conventions/time.md") {
|
|
t.Errorf("the dialogue assembled nothing:\n%s", out)
|
|
}
|
|
|
|
// Without a terminal the same bare command refuses instead of blocking on
|
|
// an answer nobody is there to give.
|
|
code, out = run(t, root, "", false, "add")
|
|
if code == cli.OK {
|
|
t.Fatalf("a bare command went through with no terminal:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "no terminal") {
|
|
t.Errorf("the refusal does not say why:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// The language is a level of its own, and a suite may keep its documents apart.
|
|
// Nothing else changes: the guide still travels next to the copies.
|
|
func TestTheLanguageMayLiveApartFromTheSuite(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
|
|
apart := filepath.Join(filepath.Dir(suiteRoot), "language")
|
|
if err := os.MkdirAll(apart, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(filepath.Join(suiteRoot, "READING.md"), filepath.Join(apart, "READING.md")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := read(t, suiteRoot, "suite.toml")
|
|
body = strings.Replace(body, `reading = "READING.md"`, "source = \"../language\"\nreading = \"READING.md\"", 1)
|
|
if err := os.WriteFile(filepath.Join(suiteRoot, "suite.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
checkClean(t, suiteRoot)
|
|
|
|
// A check that did not run says so: silence would read as a check passed.
|
|
if _, out := run(t, suiteRoot, "", false, "suite", "check"); !strings.Contains(out, "went unchecked") {
|
|
t.Errorf("the check did not say it left the language alone:\n%s", out)
|
|
}
|
|
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
if code, out := run(t, root, "", false, "add", "time"); code != cli.OK {
|
|
t.Fatalf("convy add returned %d: %s", code, out)
|
|
}
|
|
if guide := read(t, root, "docs/conventions/READING.md"); !strings.Contains(guide, "ДОПУСКАЕТСЯ") {
|
|
t.Errorf("the guide did not come from the level it lives on:\n%s", guide)
|
|
}
|
|
}
|
|
|
|
// The rules of the repository take a prefix on X and live below the marker. One
|
|
// standing above it would be wiped by the next pull, and saying so afterwards
|
|
// is too late.
|
|
func TestCheckCatchesALocalRuleAboveTheMarker(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
name := filepath.Join(root, "docs", "conventions", "time.md")
|
|
body := read(t, root, "docs/conventions/time.md")
|
|
local := "\n### XTIM-1. Часы в тестах замораживаются\n\n" +
|
|
"**ДОЛЖЕН.** Тест берёт время у подменённого `store.Now`.\n\n" +
|
|
"**ПОЧЕМУ.** Плавающее время делает падение теста невоспроизводимым.\n"
|
|
|
|
if err := os.WriteFile(name, []byte(body+local), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if code, out := run(t, root, "", false, "check"); code != cli.OK {
|
|
t.Fatalf("a local rule below the marker was turned down: %d\n%s", code, out)
|
|
}
|
|
|
|
above := strings.Replace(body, "<!-- conv:local -->", local+"\n<!-- conv:local -->", 1)
|
|
if err := os.WriteFile(name, []byte(above), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, out := run(t, root, "", false, "check")
|
|
if code == cli.OK {
|
|
t.Fatalf("a rule of the repository above the marker went unnoticed:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "would wipe it") {
|
|
t.Errorf("the finding does not say what is at stake:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// copyClean is the project half of the invariant worth the most: what the tool
|
|
// writes, the tool accepts — and accepts in silence, warnings included.
|
|
func copyClean(t *testing.T, root, dir string) {
|
|
t.Helper()
|
|
var docs []*doc.Document
|
|
entries, err := os.ReadDir(filepath.Join(root, filepath.FromSlash(dir)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, e := range entries {
|
|
if filepath.Ext(e.Name()) != ".md" {
|
|
continue
|
|
}
|
|
rel := path.Join(dir, e.Name())
|
|
d, err := doc.Load(rel, filepath.Join(root, filepath.FromSlash(rel)))
|
|
if err != nil {
|
|
t.Fatalf("loading %s: %v", rel, err)
|
|
}
|
|
if d.Front.Origin != "" {
|
|
docs = append(docs, d)
|
|
}
|
|
}
|
|
if len(docs) == 0 {
|
|
t.Fatalf("no copy was assembled in %s", dir)
|
|
}
|
|
rep := check.Copies(docs)
|
|
if len(rep.Findings()) == 0 {
|
|
return
|
|
}
|
|
var b strings.Builder
|
|
for _, f := range rep.Findings() {
|
|
fmt.Fprintf(&b, " %s: %s\n", f.Path, f.Msg)
|
|
}
|
|
t.Fatalf("the copies the tool assembled do not check clean:\n%s", b.String())
|
|
}
|
|
|
|
func TestWhatTheToolAssemblesTheToolAccepts(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
run(t, root, "", false, "add", "logging")
|
|
copyClean(t, root, "docs/conventions")
|
|
}
|
|
|
|
// The flag package stops at the first argument that is not a flag, so a stray
|
|
// one hides every flag written after it and the command quietly does something
|
|
// else than what was asked.
|
|
func TestProjectCommandsTurnDownAStrayArgument(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
for _, args := range [][]string{
|
|
{"pull", "backend"},
|
|
{"list", "backend"},
|
|
{"check", "backend"},
|
|
{"init", "somewhere"},
|
|
} {
|
|
t.Run(args[0], func(t *testing.T) {
|
|
code, out := run(t, root, "", false, args...)
|
|
if code == cli.OK {
|
|
t.Fatalf("the stray argument went through:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "takes no argument") {
|
|
t.Errorf("the refusal does not say what is wrong:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The marker is always left by the assembler, so a copy without one was edited
|
|
// by hand — and everything in it would count as suite text to be replaced.
|
|
func TestPullRefusesACopyWhoseMarkerIsGone(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
name := filepath.Join(root, "docs", "conventions", "time.md")
|
|
body := read(t, root, "docs/conventions/time.md")
|
|
body = strings.Replace(body, "<!-- conv:local -->", "Заметка, написанная руками.", 1)
|
|
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "pull")
|
|
if code == cli.OK {
|
|
t.Fatalf("the pull went ahead over a hand-edited copy:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "carries no <!-- conv:local --> marker") {
|
|
t.Errorf("the refusal does not say why:\n%s", out)
|
|
}
|
|
if got := read(t, root, "docs/conventions/time.md"); !strings.Contains(got, "Заметка, написанная руками") {
|
|
t.Errorf("the hand-written text was destroyed anyway:\n%s", got)
|
|
}
|
|
}
|
|
|
|
// Whatever the tool writes into the manifest it has to read back, and a
|
|
// backslash is the ordinary way that fails.
|
|
func TestInitWritesAManifestItCanRead(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", `docs\conventions`)
|
|
|
|
code, out := run(t, root, "", false, "list")
|
|
if code != cli.OK {
|
|
t.Fatalf("the manifest the tool wrote does not parse: %d\n%s", code, out)
|
|
}
|
|
if !strings.Contains(out, `docs\conventions`) {
|
|
t.Errorf("the directory came back changed:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// A typo in a key costs a whole component, and the parser knows about it — so
|
|
// every command that reads the manifest has to pass that on.
|
|
func TestCheckSaysWhatItDidNotUnderstand(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
body := read(t, root, ".conventions.toml")
|
|
body = strings.Replace(body, `dir = "docs/conventions"`, `dyr = "docs/conventions"`, 1)
|
|
if err := os.WriteFile(filepath.Join(root, ".conventions.toml"), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
code, out := run(t, root, "", false, "check")
|
|
if !strings.Contains(out, "dyr") {
|
|
t.Errorf("the typo in the manifest went unmentioned:\n%s", out)
|
|
}
|
|
if code == cli.OK {
|
|
t.Errorf("a component with no dir was checked anyway:\n%s", out)
|
|
}
|
|
}
|