Update and move testsuite to internal package (#730)
* Regenerate test suite * Move test suite to /internal
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user