tomltestgen: add toml-test unit test generation command (#610)

Tests are hidden behind a "testsuite" build tag for now since many tests
are failing.  Use `go test -tags testsuite` to activate.

Use `go generate` to regenerate toml_testgen_test.go.

Co-authored-by: Thomas Pelletier <thomas@pelletier.codes>
This commit is contained in:
Cameron Moore
2021-10-03 21:15:30 -05:00
committed by GitHub
parent 476492a85c
commit 62acca2b68
7 changed files with 1835 additions and 893 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,
}
}
+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
}
+48
View File
@@ -0,0 +1,48 @@
// 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"
"log"
"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() {
var decoded map[string]interface{}
if err := toml.NewDecoder(os.Stdin).Decode(&decoded); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}
j := json.NewEncoder(os.Stdout)
j.SetIndent("", " ")
if err := j.Encode(addTag("", decoded)); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}