package manifest_test import ( "slices" "strings" "testing" "github.com/BurntSushi/toml" "git.vakhrushev.me/av/convy/internal/manifest" ) const projectSource = `# What this repository takes. source = "../conventions" # A component is a region where every chosen layer holds at once. [components.backend] dir = "backend/docs/conventions" lang = ["go"] topics = ["errors", "time"] [components.web] dir = "web/docs/conventions" topics = [ "client-logging", ] ` func TestAddToListKeepsTheShapeOfTheArray(t *testing.T) { out, err := manifest.AddToList([]byte(projectSource), "components.backend", "topics", "logging") if err != nil { t.Fatal(err) } body := string(out) if !strings.Contains(body, `topics = ["errors", "logging", "time"]`) { t.Errorf("a sorted one-line array did not stay sorted and on one line:\n%s", body) } if !strings.Contains(body, "# A component is a region where every chosen layer holds at once.") { t.Errorf("the comment did not survive the edit:\n%s", body) } out, err = manifest.AddToList(out, "components.web", "topics", "auth") if err != nil { t.Fatal(err) } want := "topics = [\n \"auth\",\n \"client-logging\",\n]" if !strings.Contains(string(out), want) { t.Errorf("an array written down a column did not stay a column:\n%s", out) } } func TestAddToListCreatesTheKeyItDoesNotFind(t *testing.T) { out, err := manifest.AddToList([]byte(projectSource), "components.web", "stack", "express") if err != nil { t.Fatal(err) } if !strings.Contains(string(out), `stack = ["express"]`) { t.Errorf("the missing key was not created:\n%s", out) } } func TestAddToListRefusesWhatIsThereAlready(t *testing.T) { _, err := manifest.AddToList([]byte(projectSource), "components.backend", "topics", "time") if err == nil || !strings.Contains(err.Error(), "already holds") { t.Errorf("a repeated subscription went through: %v", err) } if _, err := manifest.AddToList([]byte(projectSource), "components.mobile", "topics", "time"); err == nil { t.Errorf("a table that is not there took an entry") } } func TestRemoveFromList(t *testing.T) { out, err := manifest.RemoveFromList([]byte(projectSource), "components.backend", "topics", "errors") if err != nil { t.Fatal(err) } if !strings.Contains(string(out), `topics = ["time"]`) { t.Errorf("the value was not removed:\n%s", out) } if _, err := manifest.RemoveFromList([]byte(projectSource), "components.backend", "topics", "logging"); err == nil { t.Errorf("removing what is not there went through") } } func TestAddTableGoesLast(t *testing.T) { out, err := manifest.AddTable([]byte(projectSource), "components.mobile", [][2]string{{"dir", `"mobile/docs/conventions"`}, {"topics", "[]"}}) if err != nil { t.Fatal(err) } body := string(out) if !strings.Contains(body, "[components.mobile]\ndir = \"mobile/docs/conventions\"\ntopics = []") { t.Errorf("the table was not written:\n%s", body) } if strings.Index(body, "[components.mobile]") < strings.Index(body, "[components.web]") { t.Errorf("the new table did not go last:\n%s", body) } if _, err := manifest.AddTable(out, "components.mobile", nil); err == nil { t.Errorf("a second table of the same name went through") } } // The array is read the way the manifest wrote it. A # inside a string opens no // comment, a bracket inside a comment closes no array, and a name inside a // comment is not a subscription — turning any of the three into data is the one // mistake there is no way back from. func TestAddToListReadsCommentsAsComments(t *testing.T) { cases := []struct { name string src string add string values []string remains string }{{ name: "a name quoted inside a comment", src: "[c.app]\ntopics = [\n \"errors\",\n # \"config\" is not taken yet\n]\n", add: "time", values: []string{"errors", "time"}, remains: `# "config" is not taken yet`, }, { name: "a bracket inside a comment", src: "[c.app]\ntopics = [\n \"errors\", # [enough for now]\n \"db-schema\",\n]\n", add: "config", values: []string{"errors", "db-schema", "config"}, remains: `"errors", # [enough for now]`, }, { name: "a comment trailing a one-line array", src: "[c.app]\ntopics = [\"git\"] # only git for now\n", add: "time", values: []string{"git", "time"}, remains: `topics = ["git", "time"] # only git for now`, }, { name: "a hash inside a value", src: "[c.app]\ntopics = [\"a#b\"]\n", add: "time", values: []string{"a#b", "time"}, remains: `topics = ["a#b", "time"]`, }} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { out, err := manifest.AddToList([]byte(tc.src), "c.app", "topics", tc.add) if err != nil { t.Fatal(err) } got := decodeTopics(t, out) if !slices.Equal(got, tc.values) { t.Errorf("the array holds %q, expected %q:\n%s", got, tc.values, out) } if !strings.Contains(string(out), tc.remains) { t.Errorf("what the author wrote is gone — no %q:\n%s", tc.remains, out) } }) } } // decodeTopics reads the array back with the parser the tool itself uses. func decodeTopics(t *testing.T, source []byte) []string { t.Helper() var got struct { C map[string]struct{ Topics []string } `toml:"c"` } if _, err := toml.Decode(string(source), &got); err != nil { t.Fatalf("the manifest the tool wrote does not parse: %v\n%s", err, source) } return got.C["app"].Topics } // A key is not always one line. Appending after the first line of an array // written down a column would land the new entry inside it. func TestAddToListAppendsPastAMultilineNeighbour(t *testing.T) { src := "[c.app]\ndir = \"docs\"\nstack = [\n \"sqlite\",\n \"postgres\",\n]\n" out, err := manifest.AddToList([]byte(src), "c.app", "topics", "errors") if err != nil { t.Fatal(err) } body := string(out) if !strings.Contains(body, " \"postgres\",\n]\ntopics = [\"errors\"]") { t.Errorf("the new key did not land past the array:\n%s", body) } } // Whatever the tool writes into a manifest, it has to be able to read back. func TestQuoteSurvivesTheRoundTrip(t *testing.T) { for _, value := range []string{`docs\conventions`, `a "quoted" name`, `both\ "kinds"`} { out, err := manifest.AddToList([]byte("[c.app]\ntopics = []\n"), "c.app", "topics", value) if err != nil { t.Fatal(err) } if list := decodeTopics(t, out); len(list) != 1 || list[0] != value { t.Errorf("%q came back as %q", value, list) } } }