diff --git a/testsuite/json.go b/testsuite/json.go new file mode 100644 index 0000000..9bb7988 --- /dev/null +++ b/testsuite/json.go @@ -0,0 +1,244 @@ +package testsuite + +import ( + "fmt" + "strconv" + "strings" + "testing" + "time" +) + +func CmpJSON(t *testing.T, key string, want, have interface{}) { + switch w := want.(type) { + case map[string]interface{}: + cmpJSONMaps(t, key, w, have) + case []interface{}: + cmpJSONArrays(t, key, w, have) + default: + t.Errorf( + "Key '%s' in expected output should be a map or a list of maps, but it's a %T", + key, want) + } +} + +func cmpJSONMaps(t *testing.T, key string, want map[string]interface{}, have interface{}) { + haveMap, ok := have.(map[string]interface{}) + if !ok { + mismatch(t, key, "table", want, haveMap) + return + } + + // Check to make sure both or neither are values. + if isValue(want) && !isValue(haveMap) { + t.Fatalf("Key '%s' is supposed to be a value, but the parser reports it as a table", key) + } + if !isValue(want) && isValue(haveMap) { + t.Fatalf("Key '%s' is supposed to be a table, but the parser reports it as a value", key) + } + if isValue(want) && isValue(haveMap) { + cmpJSONValues(t, key, want, haveMap) + return + } + + // Check that the keys of each map are equivalent. + for k := range want { + if _, ok := haveMap[k]; !ok { + bunk := kjoin(key, k) + t.Fatalf("Could not find key '%s' in parser output.", bunk) + } + } + for k := range haveMap { + if _, ok := want[k]; !ok { + bunk := kjoin(key, k) + t.Fatalf("Could not find key '%s' in expected output.", bunk) + } + } + + // Okay, now make sure that each value is equivalent. + for k := range want { + CmpJSON(t, kjoin(key, k), want[k], haveMap[k]) + } +} + +func cmpJSONArrays(t *testing.T, key string, want, have interface{}) { + wantSlice, ok := want.([]interface{}) + if !ok { + panic(fmt.Sprintf("'value' should be a JSON array when 'type=array', but it is a %T", want)) + } + + haveSlice, ok := have.([]interface{}) + if !ok { + t.Fatalf("Malformed output from your encoder: 'value' is not a JSON array: %T", have) + } + + if len(wantSlice) != len(haveSlice) { + t.Fatalf("Array lengths differ for key '%s':\n"+ + " Expected: %d\n"+ + " Your encoder: %d", + key, len(wantSlice), len(haveSlice)) + } + for i := 0; i < len(wantSlice); i++ { + CmpJSON(t, key, wantSlice[i], haveSlice[i]) + } +} + +func cmpJSONValues(t *testing.T, key string, want, have map[string]interface{}) { + wantType, ok := want["type"].(string) + if !ok { + panic(fmt.Sprintf("'type' should be a string, but it is a %T", want["type"])) + } + + haveType, ok := have["type"].(string) + if !ok { + t.Fatalf("Malformed output from your encoder: 'type' is not a string: %T", have["type"]) + } + + if wantType != haveType { + valMismatch(t, key, wantType, haveType, want, have) + } + + // If this is an array, then we've got to do some work to check equality. + if wantType == "array" { + cmpJSONArrays(t, key, want, have) + return + } + + // Atomic values are always strings + wantVal, ok := want["value"].(string) + if !ok { + panic(fmt.Sprintf("'value' %v should be a string, but it is a %[1]T", want["value"])) + } + + haveVal, ok := have["value"].(string) + if !ok { + panic(fmt.Sprintf("Malformed output from your encoder: %T is not a string", have["value"])) + } + + // Excepting floats and datetimes, other values can be compared as strings. + switch wantType { + case "float": + cmpFloats(t, key, wantVal, haveVal) + case "datetime", "datetime-local", "date-local", "time-local": + cmpAsDatetimes(t, key, wantType, wantVal, haveVal) + default: + cmpAsStrings(t, key, wantVal, haveVal) + } +} + +func cmpAsStrings(t *testing.T, key string, want, have string) { + if want != have { + t.Fatalf("Values for key '%s' don't match:\n"+ + " Expected: %s\n"+ + " Your encoder: %s", + key, want, have) + } +} + +func cmpFloats(t *testing.T, key string, want, have string) { + // Special case for NaN, since NaN != NaN. + if strings.HasSuffix(want, "nan") || strings.HasSuffix(have, "nan") { + if want != have { + t.Fatalf("Values for key '%s' don't match:\n"+ + " Expected: %v\n"+ + " Your encoder: %v", + key, want, have) + } + return + } + + wantF, err := strconv.ParseFloat(want, 64) + if err != nil { + panic(fmt.Sprintf("Could not read '%s' as a float value for key '%s'", want, key)) + } + + haveF, err := strconv.ParseFloat(have, 64) + if err != nil { + panic(fmt.Sprintf("Malformed output from your encoder: key '%s' is not a float: '%s'", key, have)) + } + + if wantF != haveF { + t.Fatalf("Values for key '%s' don't match:\n"+ + " Expected: %v\n"+ + " Your encoder: %v", + key, wantF, haveF) + } +} + +var datetimeRepl = strings.NewReplacer( + " ", "T", + "t", "T", + "z", "Z") + +var layouts = map[string]string{ + "datetime": time.RFC3339Nano, + "datetime-local": "2006-01-02T15:04:05.999999999", + "date-local": "2006-01-02", + "time-local": "15:04:05", +} + +func cmpAsDatetimes(t *testing.T, key string, kind, want, have string) { + layout, ok := layouts[kind] + if !ok { + panic("should never happen") + } + + wantT, err := time.Parse(layout, datetimeRepl.Replace(want)) + if err != nil { + panic(fmt.Sprintf("Could not read '%s' as a datetime value for key '%s'", want, key)) + } + + haveT, err := time.Parse(layout, datetimeRepl.Replace(want)) + if err != nil { + t.Fatalf("Malformed output from your encoder: key '%s' is not a datetime: '%s'", key, have) + return + } + if !wantT.Equal(haveT) { + t.Fatalf("Values for key '%s' don't match:\n"+ + " Expected: %v\n"+ + " Your encoder: %v", + key, wantT, haveT) + } +} + +func cmpAsDatetimesLocal(t *testing.T, key string, want, have string) { + if datetimeRepl.Replace(want) != datetimeRepl.Replace(have) { + t.Fatalf("Values for key '%s' don't match:\n"+ + " Expected: %v\n"+ + " Your encoder: %v", + key, want, have) + } +} + +func kjoin(old, key string) string { + if len(old) == 0 { + return key + } + return old + "." + key +} + +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 +} + +func mismatch(t *testing.T, key string, wantType string, want, have interface{}) { + t.Fatalf("Key '%s' is not an %s but %[4]T:\n"+ + " Expected: %#[3]v\n"+ + " Your encoder: %#[4]v", + key, wantType, want, have) +} + +func valMismatch(t *testing.T, key string, wantType, haveType string, want, have interface{}) { + t.Fatalf("Key '%s' is not an %s but %s:\n"+ + " Expected: %#[3]v\n"+ + " Your encoder: %#[4]v", + key, wantType, want, have) +} diff --git a/toml_testgen_support_test.go b/toml_testgen_support_test.go index bbfbe05..b1e2b4c 100644 --- a/toml_testgen_support_test.go +++ b/toml_testgen_support_test.go @@ -50,5 +50,5 @@ func testgenValid(t *testing.T, input string, jsonRef string) { err = json.Unmarshal([]byte(j), &actual) require.NoError(t, err) - require.Equal(t, ref, actual) + testsuite.CmpJSON(t, "", ref, actual) } diff --git a/toml_testgen_test.go b/toml_testgen_test.go index 72abd38..ae9804d 100644 --- a/toml_testgen_test.go +++ b/toml_testgen_test.go @@ -1102,7 +1102,6 @@ func TestTOMLTest_Valid_Datetime_Local(t *testing.T) { } func TestTOMLTest_Valid_Datetime_Milliseconds(t *testing.T) { - t.Skip("FIXME") input := "utc1 = 1987-07-05T17:45:56.123456Z\nutc2 = 1987-07-05T17:45:56.6Z\nwita1 = 1987-07-05T17:45:56.123456+08:00\nwita2 = 1987-07-05T17:45:56.6+08:00\n" jsonRef := "{\n \"utc1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.123456Z\"\n },\n \"utc2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.600000Z\"\n },\n \"wita1\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.123456+08:00\"\n },\n \"wita2\": {\n \"type\": \"datetime\",\n \"value\": \"1987-07-05T17:45:56.600000+08:00\"\n }\n}\n" testgenValid(t, input, jsonRef) @@ -1127,7 +1126,6 @@ func TestTOMLTest_Valid_Example(t *testing.T) { } func TestTOMLTest_Valid_Float_Exponent(t *testing.T) { - t.Skip("FIXME") input := "lower = 3e2\nupper = 3E2\nneg = 3e-2\npos = 3E+2\nzero = 3e0\npointlower = 3.1e2\npointupper = 3.1E2\nminustenth = -1E-1\n" jsonRef := "{\n \"lower\": {\n \"type\": \"float\",\n \"value\": \"300.0\"\n },\n \"minustenth\": {\n \"type\": \"float\",\n \"value\": \"-0.1\"\n },\n \"neg\": {\n \"type\": \"float\",\n \"value\": \"0.03\"\n },\n \"pointlower\": {\n \"type\": \"float\",\n \"value\": \"310.0\"\n },\n \"pointupper\": {\n \"type\": \"float\",\n \"value\": \"310.0\"\n },\n \"pos\": {\n \"type\": \"float\",\n \"value\": \"300.0\"\n },\n \"upper\": {\n \"type\": \"float\",\n \"value\": \"300.0\"\n },\n \"zero\": {\n \"type\": \"float\",\n \"value\": \"3.0\"\n }\n}\n" testgenValid(t, input, jsonRef) @@ -1140,7 +1138,6 @@ func TestTOMLTest_Valid_Float_Float(t *testing.T) { } func TestTOMLTest_Valid_Float_InfAndNan(t *testing.T) { - t.Skip("FIXME") input := "# We don't encode +nan and -nan back with the signs; many languages don't\n# support a sign on NaN (it doesn't really make much sense).\nnan = nan\nnan_neg = -nan\nnan_plus = +nan\ninfinity = inf\ninfinity_neg = -inf\ninfinity_plus = +inf\n" jsonRef := "{\n \"infinity\": {\n \"type\": \"float\",\n \"value\": \"inf\"\n },\n \"infinity_neg\": {\n \"type\": \"float\",\n \"value\": \"-inf\"\n },\n \"infinity_plus\": {\n \"type\": \"float\",\n \"value\": \"+inf\"\n },\n \"nan\": {\n \"type\": \"float\",\n \"value\": \"nan\"\n },\n \"nan_neg\": {\n \"type\": \"float\",\n \"value\": \"nan\"\n },\n \"nan_plus\": {\n \"type\": \"float\",\n \"value\": \"nan\"\n }\n}\n" testgenValid(t, input, jsonRef) @@ -1153,7 +1150,6 @@ func TestTOMLTest_Valid_Float_Long(t *testing.T) { } func TestTOMLTest_Valid_Float_Underscore(t *testing.T) { - t.Skip("FIXME") input := "before = 3_141.5927\nafter = 3141.592_7\nexponent = 3e1_4\n" jsonRef := "{\n \"after\": {\n \"type\": \"float\",\n \"value\": \"3141.5927\"\n },\n \"before\": {\n \"type\": \"float\",\n \"value\": \"3141.5927\"\n },\n \"exponent\": {\n \"type\": \"float\",\n \"value\": \"3.0e14\"\n }\n}\n" testgenValid(t, input, jsonRef)