Update and move testsuite to internal package (#730)

* Regenerate test suite

* Move test suite to /internal
This commit is contained in:
Thomas Pelletier
2021-12-31 18:52:26 -05:00
committed by GitHub
parent 4a73a200ed
commit f1391952d4
8 changed files with 128 additions and 27 deletions
+74
View File
@@ -0,0 +1,74 @@
package testsuite
import (
"fmt"
"math"
"time"
"github.com/pelletier/go-toml/v2"
)
// addTag adds JSON tags to a data structure as expected by toml-test.
func addTag(key string, tomlData interface{}) interface{} {
// Switch on the data type.
switch orig := tomlData.(type) {
default:
//return map[string]interface{}{}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
// A table: we don't need to add any tags, just recurse for every table
// entry.
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = addTag(k, v)
}
return typed
// An array: we don't need to add any tags, just recurse for every table
// entry.
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = addTag("", v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = addTag("", v)
}
return typed
// Datetime: tag as datetime.
case toml.LocalTime:
return tag("time-local", orig.String())
case toml.LocalDate:
return tag("date-local", orig.String())
case toml.LocalDateTime:
return tag("datetime-local", orig.String())
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
// Tag primitive values: bool, string, int, and float64.
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
// Special case for nan since NaN == NaN is false.
if math.IsNaN(orig) {
return tag("float", "nan")
}
return tag("float", fmt.Sprintf("%v", orig))
}
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+244
View File
@@ -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)
}
+69
View File
@@ -0,0 +1,69 @@
package testsuite
import (
"bytes"
"encoding/json"
"fmt"
"github.com/pelletier/go-toml/v2"
)
type parser struct{}
func (p parser) Decode(input string) (output string, outputIsError bool, retErr error) {
defer func() {
if r := recover(); r != nil {
switch rr := r.(type) {
case error:
retErr = rr
default:
retErr = fmt.Errorf("%s", rr)
}
}
}()
var v interface{}
if err := toml.Unmarshal([]byte(input), &v); err != nil {
return err.Error(), true, nil
}
j, err := json.MarshalIndent(addTag("", v), "", " ")
if err != nil {
return "", false, retErr
}
return string(j), false, retErr
}
func (p parser) Encode(input string) (output string, outputIsError bool, retErr error) {
defer func() {
if r := recover(); r != nil {
switch rr := r.(type) {
case error:
retErr = rr
default:
retErr = fmt.Errorf("%s", rr)
}
}
}()
var tmp interface{}
err := json.Unmarshal([]byte(input), &tmp)
if err != nil {
return "", false, err
}
rm, err := rmTag(tmp)
if err != nil {
return err.Error(), true, retErr
}
buf := new(bytes.Buffer)
err = toml.NewEncoder(buf).Encode(rm)
if err != nil {
return err.Error(), true, retErr
}
return buf.String(), false, retErr
}
+110
View File
@@ -0,0 +1,110 @@
package testsuite
import (
"fmt"
"strconv"
"time"
)
// Remove JSON tags to a data structure as returned by toml-test.
func rmTag(typedJson interface{}) (interface{}, error) {
// Check if key is in the table m.
in := func(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}
// Switch on the data type.
switch v := typedJson.(type) {
// Object: this can either be a TOML table or a primitive with tags.
case map[string]interface{}:
// This value represents a primitive: remove the tags and return just
// the primitive value.
if len(v) == 2 && in("type", v) && in("value", v) {
ut, err := untag(v)
if err != nil {
return ut, fmt.Errorf("tag.Remove: %w", err)
}
return ut, nil
}
// Table: remove tags on all children.
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
var err error
m[k], err = rmTag(v2)
if err != nil {
return nil, err
}
}
return m, nil
// Array: remove tags from all itenm.
case []interface{}:
a := make([]interface{}, len(v))
for i := range v {
var err error
a[i], err = rmTag(v[i])
if err != nil {
return nil, err
}
}
return a, nil
}
// The top level must be an object or array.
return nil, fmt.Errorf("unrecognized JSON format '%T'", typedJson)
}
// Return a primitive: read the "type" and convert the "value" to that.
func untag(typed map[string]interface{}) (interface{}, error) {
t := typed["type"].(string)
v := typed["value"].(string)
switch t {
case "string":
return v, nil
case "integer":
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return n, nil
case "float":
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil, fmt.Errorf("untag: %w", err)
}
return f, nil
case "datetime":
return parseTime(v, "2006-01-02T15:04:05.999999999Z07:00", false)
case "datetime-local":
return parseTime(v, "2006-01-02T15:04:05.999999999", true)
case "date-local":
return parseTime(v, "2006-01-02", true)
case "time-local":
return parseTime(v, "15:04:05.999999999", true)
case "bool":
switch v {
case "true":
return true, nil
case "false":
return false, nil
}
return nil, fmt.Errorf("untag: could not parse %q as a boolean", v)
}
return nil, fmt.Errorf("untag: unrecognized tag type %q", t)
}
func parseTime(v, format string, local bool) (t time.Time, err error) {
if local {
t, err = time.ParseInLocation(format, v, time.Local)
} else {
t, err = time.Parse(format, v)
}
if err != nil {
return time.Time{}, fmt.Errorf("Could not parse %q as a datetime: %w", v, err)
}
return t, nil
}
+50
View File
@@ -0,0 +1,50 @@
// Package testsuite provides helper functions for interoperating with the
// language-agnostic TOML test suite at github.com/BurntSushi/toml-test.
package testsuite
import (
"encoding/json"
"fmt"
"os"
"github.com/pelletier/go-toml/v2"
)
// Marshal is a helpfer function for calling toml.Marshal
//
// Only needed to avoid package import loops.
func Marshal(v interface{}) ([]byte, error) {
return toml.Marshal(v)
}
// Unmarshal is a helper function for calling toml.Unmarshal.
//
// Only needed to avoid package import loops.
func Unmarshal(data []byte, v interface{}) error {
return toml.Unmarshal(data, v)
}
// ValueToTaggedJSON takes a data structure and returns the tagged JSON
// representation.
func ValueToTaggedJSON(doc interface{}) ([]byte, error) {
return json.MarshalIndent(addTag("", doc), "", " ")
}
// DecodeStdin is a helper function for the toml-test binary interface. TOML input
// is read from STDIN and a resulting tagged JSON representation is written to
// STDOUT.
func DecodeStdin() error {
var decoded map[string]interface{}
if err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
return fmt.Errorf("Error decoding TOML: %s", err)
}
j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(addTag("", decoded)); err != nil {
return fmt.Errorf("Error encoding JSON: %s", err)
}
return nil
}