Files
convy/internal/cli/sync_test.go
T
av 92bd1f463d манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
2026-07-28 09:45:10 +03:00

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
}