Merge pull request #14 from pelletier/pelletier/go_test_support
Add BurntSushi's test suite
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1b6190
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+test_program/test_program_bin
diff --git a/.travis.yml b/.travis.yml
index 17a616a..ea2966c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,5 @@
language: go
+script: "./test.sh"
go:
- 1.0
- 1.1
diff --git a/README.md b/README.md
index 334d927..756ed51 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,14 @@
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
much appreciated!
+### Run tests
+
+You have to make sure two kind of tests run:
+
+1. The Go unit tests: `go test`
+2. The TOML examples base: `./test_program/go-test.sh`
+
+You can run both of them using `./test.sh`.
## License
diff --git a/lexer.go b/lexer.go
index 2593466..5dfff38 100644
--- a/lexer.go
+++ b/lexer.go
@@ -68,7 +68,9 @@
}
func isKeyChar(r rune) bool {
- return isAlphanumeric(r) || r == '-'
+ // "Keys start with the first non-whitespace character and end with the last
+ // non-whitespace character before the equals sign."
+ return !(isSpace(r) || r == '\n' || r == eof || r == '=')
}
func isDigit(r rune) bool {
@@ -167,10 +169,6 @@
return lexEqual
}
- if isAlphanumeric(next) {
- return lexKey
- }
-
if isSpace(next) {
l.ignore()
}
@@ -179,6 +177,10 @@
return lexRvalue
}
+ if isKeyChar(next) {
+ return lexKey
+ }
+
if l.next() == eof {
break
}
@@ -192,6 +194,10 @@
for {
next := l.peek()
switch next {
+ case '.':
+ return l.errorf("cannot start float with a dot")
+ case '=':
+ return l.errorf("cannot have multiple equals for the same key")
case '[':
l.depth += 1
return lexLeftBracket
@@ -328,6 +334,15 @@
} else if l.follow("\\n") {
l.pos += 1
growing_string += "\n"
+ } else if l.follow("\\b") {
+ l.pos += 1
+ growing_string += "\b"
+ } else if l.follow("\\f") {
+ l.pos += 1
+ growing_string += "\f"
+ } else if l.follow("\\/") {
+ l.pos += 1
+ growing_string += "/"
} else if l.follow("\\t") {
l.pos += 1
growing_string += "\t"
@@ -354,6 +369,9 @@
return l.errorf("invalid unicode escape: \\u" + code)
}
growing_string += string(rune(intcode))
+ } else if l.follow("\\") {
+ l.pos += 1
+ return l.errorf("invalid escape sequence: \\" + string(l.peek()))
} else {
growing_string += string(l.peek())
}
@@ -409,6 +427,12 @@
for {
next := l.next()
if next == '.' {
+ if point_seen {
+ return l.errorf("cannot have two dots in one float")
+ }
+ if !isDigit(l.peek()) {
+ return l.errorf("float cannot end with a dot")
+ }
point_seen = true
} else if isDigit(next) {
digit_seen = true
@@ -416,6 +440,9 @@
l.backup()
break
}
+ if point_seen && !digit_seen {
+ return l.errorf("cannot start float with a dot")
+ }
}
if !digit_seen {
diff --git a/lexer_test.go b/lexer_test.go
index e5846bd..8582520 100644
--- a/lexer_test.go
+++ b/lexer_test.go
@@ -113,6 +113,23 @@
})
}
+func TestKeyWithSharpAndEqual(t *testing.T) {
+ testFlow(t, "key#name = 5", []token{
+ token{tokenKey, "key#name"},
+ token{tokenEqual, "="},
+ token{tokenInteger, "5"},
+ token{tokenEOF, ""},
+ })
+}
+func TestKeyWithSymbolsAndEqual(t *testing.T) {
+ testFlow(t, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:' = 5", []token{
+ token{tokenKey, "~!@#$^&*()_+-`1234567890[]\\|/?><.,;:'"},
+ token{tokenEqual, "="},
+ token{tokenInteger, "5"},
+ token{tokenEOF, ""},
+ })
+}
+
func TestKeyEqualStringEscape(t *testing.T) {
testFlow(t, "foo = \"hello\\\"\"", []token{
token{tokenKey, "foo"},
@@ -268,6 +285,52 @@
})
}
+func TestFloatEndingWithDot(t *testing.T) {
+ testFlow(t, "foo = 42.", []token{
+ token{tokenKey, "foo"},
+ token{tokenEqual, "="},
+ token{tokenError, "float cannot end with a dot"},
+ })
+}
+
+func TestFloatWithTwoDots(t *testing.T) {
+ testFlow(t, "foo = 4.2.", []token{
+ token{tokenKey, "foo"},
+ token{tokenEqual, "="},
+ token{tokenError, "cannot have two dots in one float"},
+ })
+}
+
+func TestDoubleEqualKey(t *testing.T) {
+ testFlow(t, "foo= = 2", []token{
+ token{tokenKey, "foo"},
+ token{tokenEqual, "="},
+ token{tokenError, "cannot have multiple equals for the same key"},
+ })
+}
+
+func TestInvalidEsquapeSequence(t *testing.T) {
+ testFlow(t, "foo = \"\\x\"", []token{
+ token{tokenKey, "foo"},
+ token{tokenEqual, "="},
+ token{tokenError, "invalid escape sequence: \\x"},
+ })
+}
+
+func TestNestedArrays(t *testing.T) {
+ testFlow(t, "foo = [[[]]]", []token{
+ token{tokenKey, "foo"},
+ token{tokenEqual, "="},
+ token{tokenLeftBracket, "["},
+ token{tokenLeftBracket, "["},
+ token{tokenLeftBracket, "["},
+ token{tokenRightBracket, "]"},
+ token{tokenRightBracket, "]"},
+ token{tokenRightBracket, "]"},
+ token{tokenEOF, ""},
+ })
+}
+
func TestKeyEqualNumber(t *testing.T) {
testFlow(t, "foo = 42", []token{
token{tokenKey, "foo"},
diff --git a/parser.go b/parser.go
index e9c9f52..672bb35 100644
--- a/parser.go
+++ b/parser.go
@@ -4,15 +4,18 @@
import (
"fmt"
+ "reflect"
"strconv"
+ "strings"
"time"
)
type parser struct {
- flow chan token
- tree *TomlTree
- tokensBuffer []token
- currentGroup string
+ flow chan token
+ tree *TomlTree
+ tokensBuffer []token
+ currentGroup []string
+ seenGroupKeys []string
}
type parserStateFn func(*parser) parserStateFn
@@ -86,9 +89,15 @@
if key.typ != tokenKeyGroup {
panic(fmt.Sprintf("unexpected token %s, was expecting a key group", key))
}
+ for _, item := range p.seenGroupKeys {
+ if item == key.val {
+ panic("duplicated tables")
+ }
+ }
+ p.seenGroupKeys = append(p.seenGroupKeys, key.val)
p.tree.createSubTree(key.val)
p.assume(tokenRightBracket)
- p.currentGroup = key.val
+ p.currentGroup = strings.Split(key.val, ".")
return parseStart(p)
}
@@ -96,11 +105,17 @@
key := p.getToken()
p.assume(tokenEqual)
value := parseRvalue(p)
- final_key := key.val
- if p.currentGroup != "" {
- final_key = p.currentGroup + "." + key.val
+ var final_key []string
+ if len(p.currentGroup) > 0 {
+ final_key = p.currentGroup
+ } else {
+ final_key = make([]string, 0)
}
- p.tree.Set(final_key, value)
+ final_key = append(final_key, key.val)
+ if p.tree.GetPath(final_key) != nil {
+ panic(fmt.Sprintf("the following key was defined twice: %s", strings.Join(final_key, ".")))
+ }
+ p.tree.SetPath(final_key, value)
return parseStart(p)
}
@@ -137,6 +152,8 @@
return val
case tokenLeftBracket:
return parseArray(p)
+ case tokenError:
+ panic(tok.val)
}
panic("never reached")
@@ -146,6 +163,7 @@
func parseArray(p *parser) []interface{} {
array := make([]interface{}, 0)
+ arrayType := reflect.TypeOf(nil)
for {
follow := p.peek()
if follow == nil || follow.typ == tokenEOF {
@@ -156,14 +174,18 @@
return array
}
val := parseRvalue(p)
+ if arrayType == nil {
+ arrayType = reflect.TypeOf(val)
+ }
+ if reflect.TypeOf(val) != arrayType {
+ panic("mixed types in array")
+ }
array = append(array, val)
follow = p.peek()
if follow == nil {
panic("unterminated array")
}
if follow.typ != tokenRightBracket && follow.typ != tokenComma {
- fmt.Println(follow.typ)
- fmt.Println(follow.val)
panic("missing comma")
}
if follow.typ == tokenComma {
@@ -176,10 +198,11 @@
func parse(flow chan token) *TomlTree {
result := make(TomlTree)
parser := &parser{
- flow: flow,
- tree: &result,
- tokensBuffer: make([]token, 0),
- currentGroup: "",
+ flow: flow,
+ tree: &result,
+ tokensBuffer: make([]token, 0),
+ currentGroup: make([]string, 0),
+ seenGroupKeys: make([]string, 0),
}
parser.run()
return parser.tree
diff --git a/parser_test.go b/parser_test.go
index 629e0a6..238c3c5 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -12,9 +12,15 @@
return
}
for k, v := range ref {
- if fmt.Sprintf("%v", tree.Get(k)) != fmt.Sprintf("%v", v) {
- t.Log("was expecting", v, "at", k, "but got", tree.Get(k))
- t.Error()
+ node := tree.Get(k)
+ switch node.(type) {
+ case *TomlTree:
+ assertTree(t, node.(*TomlTree), err, v.(map[string]interface{}))
+ default:
+ if fmt.Sprintf("%v", node) != fmt.Sprintf("%v", v) {
+ t.Log("was expecting", v, "at", k, "but got", node)
+ t.Error()
+ }
}
}
}
@@ -142,6 +148,26 @@
})
}
+func TestNestedEmptyArrays(t *testing.T) {
+ tree, err := Load("a = [[[]]]")
+ assertTree(t, tree, err, map[string]interface{}{
+ "a": [][][]interface{}{[][]interface{}{[]interface{}{}}},
+ })
+}
+
+
+func TestArrayMixedTypes(t *testing.T) {
+ _, err := Load("a = [42, 16.0]")
+ if err.Error() != "mixed types in array" {
+ t.Error("Bad error message:", err.Error())
+ }
+
+ _, err = Load("a = [42, \"hello\"]")
+ if err.Error() != "mixed types in array" {
+ t.Error("Bad error message:", err.Error())
+ }
+}
+
func TestArrayNestedStrings(t *testing.T) {
tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]")
assertTree(t, tree, err, map[string]interface{}{
@@ -184,6 +210,53 @@
})
}
+func TestDuplicateGroups(t *testing.T) {
+ _, err := Load("[foo]\na=2\n[foo]b=3")
+ if err.Error() != "duplicated tables" {
+ t.Error("Bad error message:", err.Error())
+ }
+}
+
+func TestDuplicateKeys(t *testing.T) {
+ _, err := Load("foo = 2\nfoo = 3")
+ if err.Error() != "the following key was defined twice: foo" {
+ t.Error("Bad error message:", err.Error())
+ }
+}
+
+func TestEmptyIntermediateTable(t *testing.T) {
+ _, err := Load("[foo..bar]")
+ if err.Error() != "empty intermediate table" {
+ t.Error("Bad error message:", err.Error())
+ }
+}
+
+func TestImplicitDeclarationBefore(t *testing.T) {
+ tree, err := Load("[a.b.c]\nanswer = 42\n[a]\nbetter = 43")
+ assertTree(t, tree, err, map[string]interface{}{
+ "a": map[string]interface{}{
+ "b": map[string]interface{}{
+ "c": map[string]interface{}{
+ "answer": int64(42),
+ },
+ },
+ "better": int64(43),
+ },
+ })
+}
+
+func TestFloatsWithoutLeadingZeros(t *testing.T) {
+ _, err := Load("a = .42")
+ if err.Error() != "cannot start float with a dot" {
+ t.Error("Bad error message:", err.Error())
+ }
+
+ _, err = Load("a = -.42")
+ if err.Error() != "cannot start float with a dot" {
+ t.Error("Bad error message:", err.Error())
+ }
+}
+
func TestMissingFile(t *testing.T) {
_, err := LoadFile("foo.toml")
if err.Error() != "open foo.toml: no such file or directory" {
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..b6ed4b4
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# Run basic go unit tests
+go test -v ./...
+result=$?
+
+# Run example-based toml tests
+cd test_program && ./go-test.sh
+result="$(( result || $? ))"
+
+exit $result
diff --git a/test_program/go-test.sh b/test_program/go-test.sh
new file mode 100755
index 0000000..71293a3
--- /dev/null
+++ b/test_program/go-test.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+go get github.com/BurntSushi/toml-test # install test suite
+go get github.com/BurntSushi/toml/toml-test-go # install my parser
+go build -o test_program_bin github.com/pelletier/go-toml/test_program
+
+toml_test_wrapper() {
+ ret=0
+ if hash toml-test 2>/dev/null; then # test availability in $PATH
+ toml-test "$@"
+ ret=$?
+ else
+ p="$HOME/gopath/bin/toml-test" # try in Travi's place
+ if [ -f "$p" ]; then
+ "$p" "$@"
+ ret=$?
+ else
+ "$GOPATH/bin/toml-test" "$@"
+ ret=$?
+ fi
+ fi
+}
+
+toml_test_wrapper ./test_program_bin | tee test_out
+ret="$([ `tail -n 1 test_out | sed -E 's/^.+([0-9]+) failed$/\1/'` -eq 0 ])"
+exit $ret
diff --git a/test_program/test_program.go b/test_program/test_program.go
new file mode 100644
index 0000000..8912c05
--- /dev/null
+++ b/test_program/test_program.go
@@ -0,0 +1,75 @@
+package main
+
+import (
+ "io/ioutil"
+ "os"
+ "github.com/pelletier/go-toml"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+)
+
+func main() {
+ bytes, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ os.Exit(2)
+ }
+ tree, err := toml.Load(string(bytes))
+ if err != nil {
+ os.Exit(1)
+ }
+
+ typedTree := translate((map[string]interface{})(*tree))
+
+ if err := json.NewEncoder(os.Stdout).Encode(typedTree); err != nil {
+ log.Fatalf("Error encoding JSON: %s", err)
+ }
+
+ os.Exit(0)
+}
+
+func translate(tomlData interface{}) interface{} {
+
+ switch orig := tomlData.(type) {
+ case map[string]interface{}:
+ typed := make(map[string]interface{}, len(orig))
+ for k, v := range orig {
+ typed[k] = translate(v)
+ }
+ return typed
+ case *toml.TomlTree:
+ return translate((map[string]interface{})(*orig))
+ case []map[string]interface{}:
+ typed := make([]map[string]interface{}, len(orig))
+ for i, v := range orig {
+ typed[i] = translate(v).(map[string]interface{})
+ }
+ return typed
+ case []interface{}:
+ typed := make([]interface{}, len(orig))
+ for i, v := range orig {
+ typed[i] = translate(v)
+ }
+ return tag("array", typed)
+ case time.Time:
+ return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
+ case bool:
+ return tag("bool", fmt.Sprintf("%v", orig))
+ case int64:
+ return tag("integer", fmt.Sprintf("%d", orig))
+ case float64:
+ return tag("float", fmt.Sprintf("%v", orig))
+ case string:
+ return tag("string", orig)
+ }
+
+ panic(fmt.Sprintf("Unknown type: %T", tomlData))
+}
+
+func tag(typeName string, data interface{}) map[string]interface{} {
+ return map[string]interface{}{
+ "type": typeName,
+ "value": data,
+ }
+}
diff --git a/toml.go b/toml.go
index 410bab6..dbf8ae8 100644
--- a/toml.go
+++ b/toml.go
@@ -15,6 +15,18 @@
// This is the result of the parsing of a TOML file.
type TomlTree map[string]interface{}
+// Has returns a boolean indicating if the toplevel tree contains the given
+// key.
+func (t *TomlTree) Has(key string) bool {
+ mp := (map[string]interface{})(*t)
+ for k, _ := range mp {
+ if k == key {
+ return true
+ }
+ }
+ return false
+}
+
// Keys returns the keys of the toplevel tree.
// Warning: this is a costly operation.
func (t *TomlTree) Keys() []string {
@@ -30,8 +42,11 @@
// Key is a dot-separated path (e.g. a.b.c).
// Returns nil if the path does not exist in the tree.
func (t *TomlTree) Get(key string) interface{} {
+ return t.GetPath(strings.Split(key, "."))
+}
+
+func (t *TomlTree) GetPath(keys []string) interface{} {
subtree := t
- keys := strings.Split(key, ".")
for _, intermediate_key := range keys[:len(keys)-1] {
_, exists := (*subtree)[intermediate_key]
if !exists {
@@ -55,8 +70,11 @@
// Key is a dot-separated path (e.g. a.b.c).
// Creates all necessary intermediates trees, if needed.
func (t *TomlTree) Set(key string, value interface{}) {
+ t.SetPath(strings.Split(key, "."), value)
+}
+
+func (t *TomlTree) SetPath(keys []string, value interface{}) {
subtree := t
- keys := strings.Split(key, ".")
for _, intermediate_key := range keys[:len(keys)-1] {
_, exists := (*subtree)[intermediate_key]
if !exists {
@@ -76,6 +94,9 @@
func (t *TomlTree) createSubTree(key string) {
subtree := t
for _, intermediate_key := range strings.Split(key, ".") {
+ if intermediate_key == "" {
+ panic("empty intermediate table")
+ }
_, exists := (*subtree)[intermediate_key]
if !exists {
var new_tree TomlTree = make(TomlTree)
@@ -109,6 +130,5 @@
s := string(buff)
tree, err = Load(s)
}
-
return
}