go-toml/parse/testfiles_test.go

288 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"
)
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) {
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.Run(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.
if isValue(expected) && !isValue(actual) {
return fmt.Errorf("Key '%s' is supposed to be a value, but the parser reports it as a table.", key)
}
if !isValue(expected) && isValue(actual) {
return fmt.Errorf("Key '%s' is supposed to be a table, but the parser reports it as a value.", key)
}
if isValue(expected) && isValue(actual) {
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
}