292 lines
8.4 KiB
Go
292 lines
8.4 KiB
Go
package parse_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.makaay.nl/mauricem/go-toml/parse"
|
|
"github.com/pkg/profile"
|
|
)
|
|
|
|
func Test_Invalid(t *testing.T) {
|
|
for _, suite := range getTestSuites("invalid") {
|
|
success := 0
|
|
fail := 0
|
|
for name, testCase := range getTestCasesForSuite(suite) {
|
|
reader, err := os.Open(testCase.toml)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Cannot read file (%s): %s", testCase.toml, err))
|
|
}
|
|
if _, err = parse.Run(reader); err == nil {
|
|
t.Errorf("[%s] expected a parse error, but got none", name)
|
|
fail++
|
|
} else {
|
|
success++
|
|
}
|
|
}
|
|
t.Logf("Suite %s: successful = %d, failed = %d", suite.name, success, fail)
|
|
}
|
|
}
|
|
|
|
func Test_Valid(t *testing.T) {
|
|
defer profile.Start().Stop()
|
|
for _, suite := range getTestSuites("valid") {
|
|
success := 0
|
|
fail := 0
|
|
for name, testCase := range getTestCasesForSuite(suite) {
|
|
input, err := os.Open(testCase.toml)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("Cannot open toml file for test (%v): %s", testCase, err))
|
|
}
|
|
tomlTable, err := parse.RunWithoutSanityChecks(input)
|
|
if err != nil {
|
|
t.Errorf("[%s] parse failed unexpectedly: %s", name, err)
|
|
fail++
|
|
continue
|
|
}
|
|
jsonfh, err := os.Open(testCase.json)
|
|
if err != nil {
|
|
t.Errorf("[%s] Could not open JSON file (%s): %s", name, testCase.json, err)
|
|
fail++
|
|
continue
|
|
}
|
|
var expected interface{}
|
|
if err := json.NewDecoder(jsonfh).Decode(&expected); err != nil {
|
|
t.Errorf("[%s] Could not read JSON from (%s): %s", name, testCase.json, err)
|
|
fail++
|
|
continue
|
|
}
|
|
var actual interface{}
|
|
if err := json.Unmarshal([]byte(tomlTable.MakeSushi()), &actual); err != nil {
|
|
t.Errorf("[%s] Could not convert parser output to BurntSushi format: %s", name, err)
|
|
fail++
|
|
continue
|
|
}
|
|
if err := cmpJson("", expected, actual); err != nil {
|
|
t.Errorf("[%s] Expected result does not match the actual result: %s", name, err)
|
|
fail++
|
|
continue
|
|
}
|
|
success++
|
|
}
|
|
t.Logf("Suite %s: successful = %d, failed = %d", suite.name, success, fail)
|
|
}
|
|
}
|
|
|
|
type testSuite struct {
|
|
tesactualType string
|
|
name string
|
|
dir string
|
|
}
|
|
|
|
func getTestSuites(tesactualType string) []testSuite {
|
|
dir := getTestfilesDir() + "/" + tesactualType
|
|
entries, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
log.Fatalf("Cannot read directory (%s): %s", dir, err)
|
|
}
|
|
suites := make([]testSuite, len(entries))
|
|
for i, ent := range entries {
|
|
suites[i] = testSuite{tesactualType, ent.Name(), dir + "/" + ent.Name()}
|
|
}
|
|
return suites
|
|
}
|
|
|
|
func getTestfilesDir() string {
|
|
_, filename, _, _ := runtime.Caller(0)
|
|
return strings.Replace(filename, "_test.go", "", 1)
|
|
}
|
|
|
|
type testCase struct {
|
|
toml string
|
|
json string
|
|
}
|
|
|
|
func getTestCasesForSuite(suite testSuite) map[string]*testCase {
|
|
entries, err := ioutil.ReadDir(suite.dir)
|
|
if err != nil {
|
|
log.Fatalf("Cannot read directory (%s): %s", suite.dir, err)
|
|
}
|
|
testCases := make(map[string]*testCase)
|
|
for _, ent := range entries {
|
|
name := ent.Name()
|
|
id := name[0 : len(name)-5]
|
|
key := suite.tesactualType + "/" + suite.name + "/" + id
|
|
c, ok := testCases[key]
|
|
if !ok {
|
|
c = &testCase{}
|
|
testCases[key] = c
|
|
}
|
|
switch {
|
|
case strings.HasSuffix(name, ".toml"):
|
|
c.toml = suite.dir + "/" + name
|
|
case strings.HasSuffix(name, ".json"):
|
|
c.json = suite.dir + "/" + name
|
|
default:
|
|
panic("Invalid test suite file found: " + suite.dir + "/" + name)
|
|
}
|
|
}
|
|
return testCases
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inspired by https://github.com/BurntSushi/toml-test/blob/master/json.go
|
|
// I started out by using reflect.DeepEqual, but that solution does not
|
|
// take possibly different ordering of object keys into account.
|
|
// I do a lot less active checking on type assertions here, since this
|
|
// is only used for my local test sets and local parser code. I can live
|
|
// with a panic when I crap up my test data.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func cmpJson(key string, expected interface{}, actual interface{}) error {
|
|
switch expected := expected.(type) {
|
|
case map[string]interface{}:
|
|
return cmpJsonObjects(key, expected, actual)
|
|
case []interface{}:
|
|
return cmpJsonArrays(key, expected, actual)
|
|
default:
|
|
return fmt.Errorf("Key '%s' in expected output should be a map or a list of maps, but it's a %T.", key, expected)
|
|
}
|
|
}
|
|
|
|
func cmpJsonObjects(key string, expected map[string]interface{}, a interface{}) error {
|
|
actual, _ := a.(map[string]interface{})
|
|
|
|
// Check to make sure both or neither are values.
|
|
eIsVal := isValue(expected)
|
|
aIsVal := isValue(actual)
|
|
if eIsVal && !aIsVal {
|
|
return fmt.Errorf("Key '%s' is supposed to be a value, but the parser reports it as a table.", key)
|
|
}
|
|
if !eIsVal && aIsVal {
|
|
return fmt.Errorf("Key '%s' is supposed to be a table, but the parser reports it as a value.", key)
|
|
}
|
|
if eIsVal && aIsVal {
|
|
return cmpJsonValues(key, expected, actual)
|
|
}
|
|
|
|
// We've got two maps. Check if the keys are equivalent (order does not match).
|
|
for k, _ := range expected {
|
|
if _, ok := actual[k]; !ok {
|
|
return fmt.Errorf("Could not find key '%s.%s' in parser output.", key, k)
|
|
}
|
|
}
|
|
for k, _ := range actual {
|
|
if _, ok := expected[k]; !ok {
|
|
return fmt.Errorf("Could not find key '%s.%s' expected output.", key, k)
|
|
}
|
|
}
|
|
|
|
// Check if the values are equivalent.
|
|
for k, _ := range expected {
|
|
subKey := k
|
|
if key != "" {
|
|
subKey = key + "." + k
|
|
}
|
|
if err := cmpJson(subKey, expected[k], actual[k]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cmpJsonArrays(key string, expected, actual interface{}) error {
|
|
expectedArray, _ := expected.([]interface{})
|
|
actualArray, _ := actual.([]interface{})
|
|
if len(expectedArray) != len(actualArray) {
|
|
return fmt.Errorf("Array lengths differ for key '%s'. Expected a length of %d but got %d.",
|
|
key, len(expectedArray), len(actualArray))
|
|
}
|
|
for i := 0; i < len(expectedArray); i++ {
|
|
if err := cmpJson(key, expectedArray[i], actualArray[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cmpJsonValues(key string, expected, actual map[string]interface{}) error {
|
|
expectedType, _ := expected["type"].(string)
|
|
actualType, _ := actual["type"].(string)
|
|
if expectedType != actualType {
|
|
return fmt.Errorf("Type mismatch for key '%s'. Expected %s but got %s.", key, expectedType, actualType)
|
|
}
|
|
|
|
if expectedType == "array" {
|
|
return cmpJsonArrays(key, expected["value"], actual["value"])
|
|
}
|
|
|
|
// Except for floats and datetimes, other values can be compared as strings.
|
|
expectedValue, _ := expected["value"].(string)
|
|
actualValue, _ := actual["value"].(string)
|
|
switch expectedType {
|
|
case "float":
|
|
return cmpFloats(key, expectedValue, actualValue)
|
|
case "datetime":
|
|
return cmpAsDatetimes(key, expectedValue, actualValue)
|
|
default:
|
|
return cmpAsStrings(key, expectedValue, actualValue)
|
|
}
|
|
}
|
|
|
|
func cmpAsStrings(key, expected, actual string) error {
|
|
if expected != actual {
|
|
return fmt.Errorf("Values for key '%s' don't match. Expected a value of %q but got %q.", key, expected, actual)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cmpFloats(key, expected, actual string) error {
|
|
expectedFloat, err := strconv.ParseFloat(expected, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("BUG in test case. Could not read '%s' as a float value for key '%s'.", expected, key)
|
|
}
|
|
|
|
actualFloat, err := strconv.ParseFloat(actual, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("Malformed parser output. Could not read '%s' as a float value for key '%s'.", actual, key)
|
|
}
|
|
if expectedFloat != actualFloat && !(math.IsNaN(expectedFloat) && math.IsNaN(actualFloat)) {
|
|
return fmt.Errorf("Values for key '%s' don't match. Expected a value of %v but got %v.", key, expectedFloat, actualFloat)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cmpAsDatetimes(key, expected, actual string) error {
|
|
expectedTime, err := time.Parse(time.RFC3339Nano, expected)
|
|
if err != nil {
|
|
return fmt.Errorf("BUG in test case. Could not read '%s' as a datetime value for key '%s'.", expected, key)
|
|
}
|
|
|
|
actualTime, err := time.Parse(time.RFC3339Nano, actual)
|
|
if err != nil {
|
|
return fmt.Errorf("Malformed parser output. Could not read '%s' as datetime value for key '%s'.", actual, key)
|
|
}
|
|
if !expectedTime.Equal(actualTime) {
|
|
return fmt.Errorf("Values for key '%s' don't match. Expected a value of '%v' but got '%v'.", key, expectedTime, actualTime)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isValue(m map[string]interface{}) bool {
|
|
if len(m) != 2 {
|
|
return false
|
|
}
|
|
if _, ok := m["type"]; !ok {
|
|
return false
|
|
}
|
|
if _, ok := m["value"]; !ok {
|
|
return false
|
|
}
|
|
return true
|
|
}
|