Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions cmd/tomltestgen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
//
// Within the go-toml package, run `go generate`. Otherwise, use:
//
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
package main

import (
"archive/zip"
"bytes"
"flag"
"fmt"
"go/format"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"text/template"
"time"
)

type invalid struct {
Name string
Input string
}

type valid struct {
Name string
Input string
JsonRef string
}

type testsCollection struct {
Ref string
Timestamp string
Invalid []invalid
Valid []valid
Count int
}

const srcTemplate = "// +build testsuite\n\n" +
"// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
"package toml_test\n" +
" import (\n" +
" \"testing\"\n" +
")\n" +

"{{range .Invalid}}\n" +
"func TestTOMLTest_Invalid_{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" testgenInvalid(t, input)\n" +
"}\n" +
"{{end}}\n" +
"\n" +
"{{range .Valid}}\n" +
"func TestTOMLTest_Valid_{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" jsonRef := {{.JsonRef|gostr}}\n" +
" testgenValid(t, input, jsonRef)\n" +
"}\n" +
"{{end}}\n"

func downloadTmpFile(url string) string {
log.Println("starting to download file from", url)
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()

tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
if err != nil {
panic(err)
}
defer tmpfile.Close()

copiedLen, err := io.Copy(tmpfile, resp.Body)
if err != nil {
panic(err)
}
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
}
return tmpfile.Name()
}

func kebabToCamel(kebab string) string {
camel := ""
nextUpper := true
for _, c := range kebab {
if nextUpper {
camel += strings.ToUpper(string(c))
nextUpper = false
} else if c == '-' {
nextUpper = true
} else if c == '/' {
nextUpper = true
camel += "_"
} else {
camel += string(c)
}
}
return camel
}

func readFileFromZip(f *zip.File) string {
reader, err := f.Open()
if err != nil {
panic(err)
}
defer reader.Close()
bytes, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return string(bytes)
}

func templateGoStr(input string) string {
return strconv.Quote(input)
}

var (
ref = flag.String("r", "master", "git reference")
out = flag.String("o", "", "output file")
)

func usage() {
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
flag.PrintDefaults()
}

func main() {
flag.Usage = usage
flag.Parse()

url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
resultFile := downloadTmpFile(url)
defer os.Remove(resultFile)
log.Println("file written to", resultFile)

zipReader, err := zip.OpenReader(resultFile)
if err != nil {
panic(err)
}
defer zipReader.Close()

collection := testsCollection{
Ref: *ref,
Timestamp: time.Now().Format(time.RFC3339),
}

zipFilesMap := map[string]*zip.File{}

for _, f := range zipReader.File {
zipFilesMap[f.Name] = f
}

testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
for _, f := range zipReader.File {
groups := testFileRegexp.FindStringSubmatch(f.Name)
if len(groups) > 0 {
name := kebabToCamel(groups[3])
testType := groups[2]

log.Printf("> [%s] %s\n", testType, name)

tomlContent := readFileFromZip(f)

switch testType {
case "invalid":
collection.Invalid = append(collection.Invalid, invalid{
Name: name,
Input: tomlContent,
})
collection.Count++
case "valid":
baseFilePath := groups[1]
jsonFilePath := baseFilePath + ".json"
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])

collection.Valid = append(collection.Valid, valid{
Name: name,
Input: tomlContent,
JsonRef: jsonContent,
})
collection.Count++
default:
panic(fmt.Sprintf("unknown test type: %s", testType))
}
}
}

log.Printf("Collected %d tests from toml-test\n", collection.Count)

funcMap := template.FuncMap{
"gostr": templateGoStr,
}
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
buf := new(bytes.Buffer)
err = t.Execute(buf, collection)
if err != nil {
panic(err)
}
outputBytes, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}

if *out == "" {
fmt.Println(string(outputBytes))
return
}

err = os.WriteFile(*out, outputBytes, 0644)
if err != nil {
panic(err)
}
}
74 changes: 74 additions & 0 deletions testsuite/add.go
Original file line number Diff line number Diff line change
@@ -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 changes: 69 additions & 0 deletions testsuite/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading