- манифест читается так, как записан: решётка внутри строки не открывает комментарий, скобка внутри комментария не закрывает массив, имя внутри комментария не становится подпиской; новый ключ встаёт после массива, а не внутрь него - всё записываемое проходит через manifest.Quote — обратный слэш в пути делал файл, который инструмент сам не читает - маркер локальной части переехал в doc и пропускает огороженные блоки: процитированный в примере маркер больше не считается границей, а копия без маркера не перезаписывается молча - лишний позиционный аргумент отсекается: flag прекращал разбор и прятал флаги после себя, из-за чего pull, list и check игнорировали --for - заведены тесты проверок копий, включая молчание на исправной копии
195 lines
6.3 KiB
Go
195 lines
6.3 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|