манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user