package manifest_test import ( "os" "path/filepath" "strings" "testing" "git.vakhrushev.me/av/convy/internal/manifest" ) func write(t *testing.T, dir, name, body string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } // A manifest is data, and a command that changes it rewrites it whole. The // second write of the same content has to come out the same, or every command // would leave a diff of its own on top of the one it meant. func TestSaveIsIdempotent(t *testing.T) { dir := t.TempDir() write(t, dir, manifest.Name, `[language] version = 1 lang = "ru" [topics.live] time = "время" [prefixes.live] TIME = "conventions/time.md" `) m, err := manifest.Load(dir) if err != nil { t.Fatal(err) } if err := m.Save(); err != nil { t.Fatal(err) } once, err := os.ReadFile(m.Path) if err != nil { t.Fatal(err) } again, err := manifest.Load(dir) if err != nil { t.Fatalf("the manifest the tool wrote does not load: %v\n%s", err, once) } if err := again.Save(); err != nil { t.Fatal(err) } twice, err := os.ReadFile(m.Path) if err != nil { t.Fatal(err) } if string(once) != string(twice) { t.Errorf("the second write differs from the first:\n%s\n---\n%s", once, twice) } } // A write goes out of the structs, so a key the tool does not know would be // dropped. It refuses instead: that neither loses the key nor hides it. func TestSaveRefusesWhileAKeyIsUnknown(t *testing.T) { dir := t.TempDir() write(t, dir, manifest.Name, `[language] version = 1 descriptoin = "LANGUAGE.md" `) m, err := manifest.Load(dir) if err != nil { t.Fatal(err) } before, _ := os.ReadFile(m.Path) err = m.Save() if err == nil { t.Fatal("the manifest was rewritten over a key the tool does not know") } if !strings.Contains(err.Error(), "descriptoin") { t.Errorf("the refusal does not name the key: %s", err) } after, _ := os.ReadFile(m.Path) if string(before) != string(after) { t.Errorf("the file was touched anyway:\n%s", after) } } // A name never leaves the manifest: it lives on in foreign repositories, and // one handed out twice starts pointing at something else there. func TestRetireMovesRatherThanDeletes(t *testing.T) { dir := t.TempDir() write(t, dir, manifest.Name, `[language] version = 1 [topics.live] time = "время" logging = "логирование" `) m, err := manifest.Load(dir) if err != nil { t.Fatal(err) } m.Topics.Retire("logging", "2026-07-28: свёрнута в errors") if err := m.Save(); err != nil { t.Fatal(err) } back, err := manifest.Load(dir) if err != nil { t.Fatal(err) } if back.TopicLive("logging") { t.Errorf("the topic stayed live") } if !back.TopicRetired("logging") { t.Errorf("the topic is neither live nor retired: the name is loose") } if !back.TopicLive("time") { t.Errorf("the other topic went with it") } } func TestProjectRoundTrip(t *testing.T) { dir := t.TempDir() p := &manifest.Project{ Source: "../dev-conventions#v2", Components: map[string]manifest.Component{ "backend": {Dir: `docs\conventions`, Lang: []string{"go"}, Topics: []string{"time"}}, "web": {Dir: "web/docs", Topics: []string{}}, }, Path: filepath.Join(dir, manifest.ProjectName), Root: dir, } if err := p.Save(); err != nil { t.Fatal(err) } back, err := manifest.LoadProject(dir) if err != nil { t.Fatalf("the manifest the tool wrote does not load: %v", err) } if back.Source != p.Source { t.Errorf("source came back as %q", back.Source) } // A backslash is the ordinary way a written value fails to read back. if got := back.Components["backend"].Dir; got != `docs\conventions` { t.Errorf("the directory came back as %q", got) } if len(back.Components["web"].Lang) != 0 { t.Errorf("an empty axis was written and read back as something") } back.Subscribe("web", "logging") back.Subscribe("web", "errors") if got := back.Components["web"].Topics; strings.Join(got, ",") != "errors,logging" { t.Errorf("the subscription is not kept in order: %v", got) } back.Unsubscribe("web", "errors") if got := back.Components["web"].Topics; strings.Join(got, ",") != "logging" { t.Errorf("unsubscribing left %v", got) } }