v0.2.0 edit a document in place, comments kept; name the keys a struct does not take
Test / test (push) Canceled after 0s
Test / test (push) Canceled after 0s
This commit is contained in:
6 files changed
+622
-11
No files matched your search
@@ -0,0 +1,268 @@
|
||||
package toml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const settingsDoc = `# mine
|
||||
play.hud = true # root dotted
|
||||
|
||||
[paths]
|
||||
log = "l" # beside the file
|
||||
|
||||
[play]
|
||||
# kept
|
||||
mouse = false # keys
|
||||
volume = 0.3
|
||||
|
||||
# about later
|
||||
[later] # a newer build's
|
||||
key = 1
|
||||
[later.sub]
|
||||
deep = [
|
||||
1, # one
|
||||
2,
|
||||
]
|
||||
|
||||
[audio]
|
||||
buffer_ms = 20
|
||||
`
|
||||
|
||||
// TestDocumentEditsOnlyWhatItNames: a set value keeps the rest of its line, a
|
||||
// missing key goes after its table's last key in that key's spelling, a missing
|
||||
// table is appended, a delete takes a key's lines or a table's header with every
|
||||
// key under it, and every other byte, comments and blank lines, stays.
|
||||
func TestDocumentEditsOnlyWhatItNames(t *testing.T) {
|
||||
d, err := ParseDocument([]byte(settingsDoc))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, edit := range []func() error{
|
||||
func() error { return d.Set(Key{"paths", "log"}, "logs") },
|
||||
func() error { return d.Set(Key{"play", "sfx"}, false) },
|
||||
func() error { return d.Set(Key{"presentation", "cell"}, 14.0) },
|
||||
func() error { return d.Set(Key{"audio", "buffer ms"}, 30) },
|
||||
func() error { return del(d, Key{"play", "volume"}) },
|
||||
func() error { return del(d, Key{"later"}) },
|
||||
} {
|
||||
if err := edit(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
want := `# mine
|
||||
play.hud = true # root dotted
|
||||
|
||||
[paths]
|
||||
log = "logs" # beside the file
|
||||
|
||||
[play]
|
||||
# kept
|
||||
mouse = false # keys
|
||||
sfx = false
|
||||
|
||||
# about later
|
||||
|
||||
[audio]
|
||||
buffer_ms = 20
|
||||
"buffer ms" = 30
|
||||
|
||||
[presentation]
|
||||
cell = 14.0
|
||||
`
|
||||
if got := string(d.Bytes()); got != want {
|
||||
t.Fatalf("edited:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
var s struct {
|
||||
Play struct {
|
||||
HUD bool `toml:"hud"`
|
||||
Mouse bool `toml:"mouse"`
|
||||
Sfx bool `toml:"sfx"`
|
||||
} `toml:"play"`
|
||||
}
|
||||
s.Play.Sfx = true
|
||||
if err := d.Decode(&s); err != nil || !s.Play.HUD || s.Play.Mouse || s.Play.Sfx {
|
||||
t.Errorf("decoded %+v, %v", s, err)
|
||||
}
|
||||
}
|
||||
|
||||
func del(d *Document, k Key) error {
|
||||
ok, err := d.Delete(k)
|
||||
if err == nil && !ok {
|
||||
err = fmt.Errorf("%s: nothing deleted", k)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// TestSetSpellsAKeyAsItsTableDoes: with no header, a key joins the dotted keys that
|
||||
// made its table, spelled as they are.
|
||||
func TestSetSpellsAKeyAsItsTableDoes(t *testing.T) {
|
||||
d, err := ParseDocument([]byte("play.hud = true\n\n[audio]\nbuffer_ms = 20\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.Set(Key{"play", "mouse"}, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := string(d.Bytes()), "play.hud = true\nplay.mouse = false\n\n[audio]\nbuffer_ms = 20\n"; got != want {
|
||||
t.Fatalf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDocumentRefusesAnEditItCannotPlace: under an array of tables, inside an
|
||||
// inline value, or over a table, an edit fails and the document stays as it was.
|
||||
func TestDocumentRefusesAnEditItCannotPlace(t *testing.T) {
|
||||
doc := "x = 1\nm = { a = 1 }\n[t]\nk = 1\n[[arr]]\nn = 1\n"
|
||||
d, err := ParseDocument([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, edit := range map[string]func() error{
|
||||
"array of tables": func() error { return d.Set(Key{"arr", "n"}, 2) },
|
||||
"inline set": func() error { return d.Set(Key{"m", "b"}, 2) },
|
||||
"inline delete": func() error { _, err := d.Delete(Key{"m", "a"}); return err },
|
||||
"table": func() error { return d.Set(Key{"t"}, 2) },
|
||||
"under a value": func() error { return d.Set(Key{"x", "y"}, 2) },
|
||||
} {
|
||||
if err := edit(); err == nil {
|
||||
t.Errorf("%s: the edit was taken", name)
|
||||
}
|
||||
}
|
||||
if string(d.Bytes()) != doc {
|
||||
t.Fatalf("a refused edit changed the document:\n%s", d.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmappedNamesWhatDecodeIgnores: keys no field takes, by tag or field name,
|
||||
// unexported and "-" fields included, are named once each, under arrays of tables
|
||||
// without their index; a map or interface destination takes every key under it.
|
||||
func TestUnmappedNamesWhatDecodeIgnores(t *testing.T) {
|
||||
doc := `top = 1
|
||||
skip = 2
|
||||
hidden = 3
|
||||
Named = 4
|
||||
[sub]
|
||||
ok = 1
|
||||
bad = 2
|
||||
[sub.deeper]
|
||||
x = 1
|
||||
[free]
|
||||
anything = 1
|
||||
[anyv]
|
||||
v = { w = 1 }
|
||||
[[items]]
|
||||
id = 1
|
||||
odd = 1
|
||||
[[items]]
|
||||
id = 2
|
||||
odd = 2
|
||||
[byname.one]
|
||||
ok = 1
|
||||
nope = 1
|
||||
`
|
||||
type item struct {
|
||||
ID int `toml:"id"`
|
||||
}
|
||||
type sub struct {
|
||||
OK int `toml:"ok,omitempty"`
|
||||
}
|
||||
var v struct {
|
||||
Top int `toml:"top"`
|
||||
Skip int `toml:"-"`
|
||||
hidden int
|
||||
Named int `toml:",omitempty"`
|
||||
Sub *sub `toml:"sub"`
|
||||
Free map[string]any `toml:"free"`
|
||||
Anyv any `toml:"anyv"`
|
||||
Items []item `toml:"items"`
|
||||
ByName map[string]sub `toml:"byname"`
|
||||
}
|
||||
d, err := ParseDocument([]byte(doc))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []string
|
||||
for _, k := range d.Unmapped(&v) {
|
||||
got = append(got, k.String())
|
||||
}
|
||||
want := []string{"byname.one.nope", "hidden", "items.odd", "skip", "sub.bad", "sub.deeper"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("unmapped %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeyStringQuotesWhatABareKeyCannotHold
|
||||
func TestKeyStringQuotesWhatABareKeyCannotHold(t *testing.T) {
|
||||
if got := (Key{"a", "b.c", "d e", "7"}).String(); got != `a."b.c"."d e"."7"` {
|
||||
t.Fatalf("got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzDocument: a document that parses keeps its bytes; deleting any key it sets
|
||||
// leaves a shorter document that parses without that key, and setting one or a
|
||||
// key beside it, where the edit is taken, leaves it holding the value set.
|
||||
func FuzzDocument(f *testing.F) {
|
||||
for _, s := range []string{
|
||||
settingsDoc,
|
||||
"a = 1\n[t]\nk = [1,\n2] # c\n[[r]]\nx = 1\n[[r]]\nx = 2\n",
|
||||
"t.a.b = { c = 1 }\n# end",
|
||||
"\"q.k\" = 1\r\n[\"x y\".z]\r\nv = \"#\"\r\n",
|
||||
} {
|
||||
f.Add([]byte(s))
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
d, err := ParseDocument(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if string(d.Bytes()) != string(data) {
|
||||
t.Fatal("a parsed document changed its bytes")
|
||||
}
|
||||
for _, s := range d.stmts {
|
||||
c, _ := ParseDocument(data)
|
||||
ok, err := c.Delete(s.path())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("delete of %s, which the document sets, removed nothing", s.path())
|
||||
}
|
||||
if has(c.Tree(), s.path()) {
|
||||
t.Fatalf("%s still set after its delete:\n%s", s.path(), c.Bytes())
|
||||
}
|
||||
if len(c.Bytes()) >= len(data) {
|
||||
t.Fatalf("delete of %s did not shorten the document", s.path())
|
||||
}
|
||||
}
|
||||
for _, s := range d.stmts {
|
||||
for _, k := range []Key{s.path(), append(s.path()[:len(s.path())-1:len(s.path())-1], "added")} {
|
||||
c, _ := ParseDocument(data)
|
||||
if err := c.Set(k, int64(7)); err == nil && value(c.Tree(), k) != int64(7) {
|
||||
t.Fatalf("set %s took but reads %v:\n%s", k, value(c.Tree(), k), c.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func has(tree map[string]any, k Key) bool { return value(tree, k) != nil }
|
||||
|
||||
// value is what k holds in tree, in the last table of an array of tables
|
||||
func value(tree map[string]any, k Key) any {
|
||||
var cur any = tree
|
||||
for _, s := range k {
|
||||
m, ok := cur.(map[string]any)
|
||||
if !ok {
|
||||
if a, ok := cur.([]map[string]any); ok && len(a) > 0 {
|
||||
m = a[len(a)-1]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if cur, ok = m[s]; !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
Reference in new issue
Block a user