forked from pelletier/go-toml
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tomltestgen: add toml-test unit test generation command (pelletier#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>
- Loading branch information
1 parent
476492a
commit 62acca2
Showing
7 changed files
with
1,829 additions
and
887 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.