- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
167 lines
4.9 KiB
Go
167 lines
4.9 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/cli"
|
|
"git.vakhrushev.me/av/convy/internal/manifest"
|
|
)
|
|
|
|
// subscribe edits the project manifest the way a person would: by hand, in the
|
|
// file. That is the whole premise of sync — the manifest is the truth, and the
|
|
// layout follows it.
|
|
func subscribe(t *testing.T, root string, topics ...string) {
|
|
t.Helper()
|
|
m, err := manifest.LoadProject(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := m.Components["backend"]
|
|
c.Topics = topics
|
|
m.Components["backend"] = c
|
|
if err := m.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSyncAssemblesWhatIsMissingAndRemovesWhatIsOrphaned(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
run(t, root, "", false, "add", "time")
|
|
|
|
// The manifest is edited by hand: time goes, logging comes.
|
|
subscribe(t, root, "logging")
|
|
|
|
code, out := run(t, root, "", false, "sync", "--dry-run")
|
|
if code != cli.OK {
|
|
t.Fatalf("the dry run returned %d: %s", code, out)
|
|
}
|
|
if !strings.Contains(out, "would change") {
|
|
t.Errorf("the dry run promised nothing:\n%s", out)
|
|
}
|
|
if !exists(t, root, "docs/conventions/time.md") {
|
|
t.Errorf("the dry run removed a file")
|
|
}
|
|
if exists(t, root, "docs/conventions/logging.md") {
|
|
t.Errorf("the dry run assembled a file")
|
|
}
|
|
|
|
code, out = run(t, root, "", false, "sync")
|
|
if code != cli.OK {
|
|
t.Fatalf("sync returned %d: %s", code, out)
|
|
}
|
|
if !exists(t, root, "docs/conventions/logging.md") {
|
|
t.Errorf("the subscribed topic was not assembled:\n%s", out)
|
|
}
|
|
if exists(t, root, "docs/conventions/time.md") {
|
|
t.Errorf("the copy nothing subscribes to stayed:\n%s", out)
|
|
}
|
|
|
|
// Run again: nothing left to do, and it says so.
|
|
code, out = run(t, root, "", false, "sync")
|
|
if code != cli.OK || !strings.Contains(out, "already follows the manifest") {
|
|
t.Errorf("a second sync found work to do:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// Below the marker is the one thing in the directory that exists nowhere else.
|
|
func TestSyncLeavesAnOrphanCarryingALocalPart(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 += "\nTIME-1 — МЕХАНИЗИРОВАНО: `internal/archrules`.\n"
|
|
if err := os.WriteFile(name, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
subscribe(t, root)
|
|
|
|
code, out := run(t, root, "", false, "sync")
|
|
if code == cli.OK {
|
|
t.Fatalf("an orphan with a local part went unremarked:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "local part") {
|
|
t.Errorf("the report does not say why the file was left:\n%s", out)
|
|
}
|
|
if !exists(t, root, "docs/conventions/time.md") {
|
|
t.Fatalf("the local part was destroyed:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// The manifest is the truth, so a manifest that does not hold together stops
|
|
// the command before anything is written.
|
|
func TestSyncValidatesTheManifestBeforeTouchingAnything(t *testing.T) {
|
|
suiteRoot := subscribable(t)
|
|
root := wired(t, suiteRoot, "--component", "backend", "--dir", "docs/conventions", "--lang", "go")
|
|
|
|
cases := []struct {
|
|
name string
|
|
change func(*manifest.Project)
|
|
want string
|
|
}{{
|
|
name: "a topic the suite does not declare",
|
|
change: func(m *manifest.Project) { subscribeTo(m, "billing") },
|
|
want: "no such topic",
|
|
}, {
|
|
name: "the same topic twice",
|
|
change: func(m *manifest.Project) { subscribeTo(m, "time", "time") },
|
|
want: "twice",
|
|
}, {
|
|
name: "two languages in one component",
|
|
change: func(m *manifest.Project) {
|
|
c := m.Components["backend"]
|
|
c.Lang = []string{"go", "javascript"}
|
|
m.Components["backend"] = c
|
|
},
|
|
want: "declares two languages",
|
|
}, {
|
|
name: "a topic no layer of which fits",
|
|
change: func(m *manifest.Project) {
|
|
// web-ui lives on the htmx stack only, and this component is on
|
|
// no stack at all.
|
|
subscribeTo(m, "web-ui")
|
|
},
|
|
want: "no layer of it fits",
|
|
}}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m, err := manifest.LoadProject(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tc.change(m)
|
|
if err := m.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, out := run(t, root, "", false, "sync")
|
|
if code == cli.OK {
|
|
t.Fatalf("the manifest went through:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, tc.want) {
|
|
t.Errorf("the report does not say %q:\n%s", tc.want, out)
|
|
}
|
|
if !strings.Contains(out, "nothing was touched") {
|
|
t.Errorf("the report does not say it wrote nothing:\n%s", out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func subscribeTo(m *manifest.Project, topics ...string) {
|
|
c := m.Components["backend"]
|
|
c.Topics = topics
|
|
m.Components["backend"] = c
|
|
}
|
|
|
|
func exists(t *testing.T, parts ...string) bool {
|
|
t.Helper()
|
|
_, err := os.Stat(filepath.Join(parts...))
|
|
return err == nil
|
|
}
|