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 }