Repository: BurntSushi/toml
Pull request #293 (2021-06-14 22:15:57)

Update to use new toml-test, fix a few bugs
Update to use the new toml-test from
https://github.com/BurntSushi/toml-test/pull/69

Turns out quite a few failures were just because those tests were
broken. Do'h.

And fix a few minor issues:

- nan and inf got encoded as "NaN" and "Inf".
- Detect invalid UTF-8 and error out.
- Don't allow keys as triple-quoted strings: """ key """ = "foo"
- Allow encoding blank keys ("" = 1) and escapes in keys ("\n" = 1).
- Expand string quoting with \b and \f, and other control characters as
  \u...
- Allow multiline strings to end with the quote character: """v""""

The toml-test tests will fail because of a catch-22: the toml-test needs
this version of the toml library to work for the fix in the lexer for
the multiline strings.


Patch:
diff --git a/Makefile b/Makefile
index 88b3f96..ab05a3f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,41 +1,19 @@
-TOML_TESTDIR?=tests
+# toml: TOML array element cannot contain a table
+# Dotted keys are not supported yet.
+SKIP_ENCODE?=valid/inline-table-nest,valid/key-dotted
 
-# TODO: these should be fixed
-SKIP_DECODE?=valid/array-table-array-string-backslash,\
-	     valid/inline-table-array,\
-	     valid/inline-table,\
-	     valid/nested-inline-table-array,\
-	     invalid/bad-utf8,\
-	     invalid/key-multiline,\
-	     invalid/inline-table-empty,\
-	     invalid/inline-table-nest,\
-	     valid/inline-table-empty,\
-	     valid/inline-table-nest
+# Dotted keys are not supported yet.
+SKIP_DECODE=valid/key-dotted
 
-SKIP_ENCODE?=valid/inline-table-array,\
-	     valid/inline-table,\
-	     valid/nested-inline-table-array,\
-	     valid/array-table-array-string-backslash,\
-	     valid/inline-table-empty,\
-	     valid/inline-table-nest,\
-	     valid/key-escapes
+# No easy way to see if this was a datetime or local datetime; we should extend
+# meta with new types for this, which seems like a good idea in any case.
+SKIP_DECODE+=,valid/datetime-local-date,valid/datetime-local-time,valid/datetime-local
+SKIP_ENCODE+=,valid/datetime-local-date,valid/datetime-local-time,valid/datetime-local
 
-install:
+all:
+	@e=0  # So it won't stop on the first command that fails.
 	@go install ./...
-
-test: install
-	@go test ./...
-	@toml-test -testdir="${TOML_TESTDIR}" -skip="${SKIP_DECODE}"          toml-test-decoder
-	@toml-test -testdir="${TOML_TESTDIR}" -skip="${SKIP_ENCODE}" -encoder toml-test-encoder
-
-fmt:
-	gofmt -w *.go */*.go
-	colcheck *.go */*.go
-
-tags:
-	find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
-
-push:
-	git push origin master
-	git push github master
-
+	@go test ./... || e=1
+	@toml-test -skip="${SKIP_DECODE}" toml-test-decoder || e=1
+	@toml-test -encoder -skip="${SKIP_ENCODE}" toml-test-encoder || e=1
+	@exit $e
diff --git a/decode_meta.go b/decode_meta.go
index b9914a6..6cdb708 100644
--- a/decode_meta.go
+++ b/decode_meta.go
@@ -41,8 +41,8 @@ func (md *MetaData) IsDefined(key ...string) bool {
 
 // Type returns a string representation of the type of the key specified.
 //
-// Type will return the empty string if given an empty key or a key that
-// does not exist. Keys are case sensitive.
+// Type will return the empty string if given an empty key or a key that does
+// not exist. Keys are case sensitive.
 func (md *MetaData) Type(key ...string) string {
 	fullkey := strings.Join(key, ".")
 	if typ, ok := md.types[fullkey]; ok {
@@ -68,6 +68,9 @@ func (k Key) maybeQuotedAll() string {
 }
 
 func (k Key) maybeQuoted(i int) string {
+	if k[i] == "" {
+		return `""`
+	}
 	quote := false
 	for _, c := range k[i] {
 		if !isBareKeyChar(c) {
@@ -76,7 +79,7 @@ func (k Key) maybeQuoted(i int) string {
 		}
 	}
 	if quote {
-		return "\"" + strings.Replace(k[i], "\"", "\\\"", -1) + "\""
+		return `"` + quotedReplacer.Replace(k[i]) + `"`
 	}
 	return k[i]
 }
diff --git a/encode.go b/encode.go
index 2386d3d..c6b05c8 100644
--- a/encode.go
+++ b/encode.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"math"
 	"reflect"
 	"sort"
 	"strconv"
@@ -32,11 +33,41 @@ var (
 )
 
 var quotedReplacer = strings.NewReplacer(
-	"\t", "\\t",
-	"\n", "\\n",
-	"\r", "\\r",
 	"\"", "\\\"",
 	"\\", "\\\\",
+	"\x00", `\u0000`,
+	"\x01", `\u0001`,
+	"\x02", `\u0002`,
+	"\x03", `\u0003`,
+	"\x04", `\u0004`,
+	"\x05", `\u0005`,
+	"\x06", `\u0006`,
+	"\x07", `\u0007`,
+	"\b", `\b`,
+	"\t", `\t`,
+	"\n", `\n`,
+	"\x0b", `\u000b`,
+	"\f", `\f`,
+	"\r", `\r`,
+	"\x0e", `\u000e`,
+	"\x0f", `\u000f`,
+	"\x10", `\u0010`,
+	"\x11", `\u0011`,
+	"\x12", `\u0012`,
+	"\x13", `\u0013`,
+	"\x14", `\u0014`,
+	"\x15", `\u0015`,
+	"\x16", `\u0016`,
+	"\x17", `\u0017`,
+	"\x18", `\u0018`,
+	"\x19", `\u0019`,
+	"\x1a", `\u001a`,
+	"\x1b", `\u001b`,
+	"\x1c", `\u001c`,
+	"\x1d", `\u001d`,
+	"\x1e", `\u001e`,
+	"\x1f", `\u001f`,
+	"\x7f", `\u007f`,
 )
 
 // Encoder controls the encoding of Go values to a TOML document to some
@@ -187,9 +218,23 @@ func (enc *Encoder) eElement(rv reflect.Value) {
 		reflect.Uint32, reflect.Uint64:
 		enc.wf(strconv.FormatUint(rv.Uint(), 10))
 	case reflect.Float32:
-		enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
+		f := rv.Float()
+		if math.IsNaN(f) {
+			enc.wf("nan")
+		} else if math.IsInf(f, 0) {
+			enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
+		} else {
+			enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
+		}
 	case reflect.Float64:
-		enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
+		f := rv.Float()
+		if math.IsNaN(f) {
+			enc.wf("nan")
+		} else if math.IsInf(f, 0) {
+			enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
+		} else {
+			enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
+		}
 	case reflect.Array, reflect.Slice:
 		enc.eArrayOrSliceElement(rv)
 	case reflect.Interface:
@@ -236,7 +281,6 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
 		if isNil(trv) {
 			continue
 		}
-		panicIfInvalidKey(key)
 		enc.newline()
 		enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
 		enc.newline()
@@ -245,7 +289,6 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
 }
 
 func (enc *Encoder) eTable(key Key, rv reflect.Value) {
-	panicIfInvalidKey(key)
 	if len(key) == 1 {
 		// Output an extra newline between top-level tables.
 		// (The newline isn't written if nothing else has been written though.)
@@ -465,8 +508,10 @@ func tomlArrayType(rv reflect.Value) tomlType {
 			encPanic(errArrayMixedElementTypes)
 		}
 	}
-	// If we have a nested array, then we must make sure that the nested
-	// array contains ONLY primitives.
+
+	// If we have a nested array, then we must make sure that the nested array
+	// contains ONLY primitives.
+	//
 	// This checks arbitrarily nested arrays.
 	if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
 		nest := tomlArrayType(eindirect(rv.Index(0)))
@@ -535,7 +580,6 @@ func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
 	if len(key) == 0 {
 		encPanic(errNoKey)
 	}
-	panicIfInvalidKey(key)
 	enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
 	enc.eElement(val)
 	enc.newline()
@@ -573,16 +617,3 @@ func isNil(rv reflect.Value) bool {
 		return false
 	}
 }
-
-func panicIfInvalidKey(key Key) {
-	for _, k := range key {
-		if len(k) == 0 {
-			encPanic(e("Key '%s' is not a valid table name. Key names "+
-				"cannot be empty.", key.maybeQuotedAll()))
-		}
-	}
-}
-
-func isValidKeyName(s string) bool {
-	return len(s) != 0
-}
diff --git a/lex.go b/lex.go
index e87eb98..6065c86 100644
--- a/lex.go
+++ b/lex.go
@@ -66,9 +66,9 @@ type lexer struct {
 	state stateFn
 	items chan item
 
-	// Allow for backing up up to three runes.
+	// Allow for backing up up to four runes.
 	// This is necessary because TOML contains 3-rune tokens (""" and ''').
-	prevWidths [3]int
+	prevWidths [4]int
 	nprev      int // how many of prevWidths are in use
 	// If we emit an eof, we can still back up, but it is not OK to call
 	// next again.
@@ -149,12 +149,19 @@ func (lx *lexer) next() (r rune) {
 	if lx.input[lx.pos] == '\n' {
 		lx.line++
 	}
+	lx.prevWidths[3] = lx.prevWidths[2]
 	lx.prevWidths[2] = lx.prevWidths[1]
 	lx.prevWidths[1] = lx.prevWidths[0]
-	if lx.nprev < 3 {
+	if lx.nprev < 4 {
 		lx.nprev++
 	}
+
 	r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
+	if r == utf8.RuneError {
+		lx.errorf("invalid UTF-8 byte at position %d (line %d): 0x%02x", lx.pos, lx.line, lx.input[lx.pos])
+		return utf8.RuneError
+	}
+
 	lx.prevWidths[0] = w
 	lx.pos += w
 	return r
@@ -177,6 +184,7 @@ func (lx *lexer) backup() {
 	w := lx.prevWidths[0]
 	lx.prevWidths[0] = lx.prevWidths[1]
 	lx.prevWidths[1] = lx.prevWidths[2]
+	lx.prevWidths[2] = lx.prevWidths[3]
 	lx.nprev--
 	lx.pos -= w
 	if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
@@ -368,7 +376,18 @@ func lexKeyStart(lx *lexer) stateFn {
 		lx.ignore()
 		lx.emit(itemKeyStart)
 		lx.push(lexKeyEnd)
-		return lexValue // reuse string lexing
+
+		r := lx.next()
+		lx.ignore()
+		switch r {
+		case stringStart:
+			return lexString
+		case rawStringStart:
+			return lexRawString
+		default:
+			lx.errorf("keys must be string")
+		}
+		return lexValue
 	default:
 		lx.ignore()
 		lx.emit(itemKeyStart)
@@ -635,6 +654,13 @@ func lexMultilineString(lx *lexer) stateFn {
 	case stringEnd:
 		if lx.accept(stringEnd) {
 			if lx.accept(stringEnd) {
+				// Can end in quote: """str""""
+				if lx.peek() == stringEnd {
+					lx.backup()
+					lx.backup()
+					return lexMultilineString
+				}
+
 				lx.backup()
 				lx.backup()
 				lx.backup()
@@ -688,10 +714,16 @@ func lexMultilineRawString(lx *lexer) stateFn {
 		if lx.peek() != '\n' {
 			return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
 		}
-		return lexMultilineString
+		return lexMultilineRawString
 	case rawStringEnd:
 		if lx.accept(rawStringEnd) {
 			if lx.accept(rawStringEnd) {
+				// Can end in quote: '''str''''
+				if lx.peek() == rawStringEnd {
+					lx.backup()
+					lx.backup()
+					return lexMultilineRawString
+				}
 				lx.backup()
 				lx.backup()
 				lx.backup()
diff --git a/parse.go b/parse.go
index 958deab..f5aaea9 100644
--- a/parse.go
+++ b/parse.go
@@ -59,6 +59,7 @@ func parse(data string) (p *parser, err error) {
 	if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") {
 		data = data[2:]
 	}
+
 	// Examine first few bytes for NULL bytes; this probably means it's a UTF-16
 	// file (second byte in surrogate pair being NULL). Again, do this here to
 	// avoid having to deal with UTF-8/16 stuff in the lexer.
@@ -99,6 +100,7 @@ func (p *parser) panicf(format string, v ...interface{}) {
 
 func (p *parser) next() item {
 	it := p.lx.nextItem()
+	//fmt.Printf("ITEM %-18s line %-3d → %q\n", it.typ, it.line, it.val)
 	if it.typ == itemError {
 		p.panicf("%s", it.val)
 	}


Test patch:
diff --git a/cmd/toml-test-decoder/main.go b/cmd/toml-test-decoder/main.go
index ab65da4..10e246f 100644
--- a/cmd/toml-test-decoder/main.go
+++ b/cmd/toml-test-decoder/main.go
@@ -1,5 +1,5 @@
-// Command toml-test-decoder satisfies the toml-test interface for testing
-// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
+// Command toml-test-decoder satisfies the toml-test interface for testing TOML
+// decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
 package main
 
 import (
@@ -7,6 +7,7 @@ import (
 	"flag"
 	"fmt"
 	"log"
+	"math"
 	"os"
 	"path"
 	"time"
@@ -16,7 +17,6 @@ import (
 
 func init() {
 	log.SetFlags(0)
-
 	flag.Usage = usage
 	flag.Parse()
 }
@@ -24,7 +24,6 @@ func init() {
 func usage() {
 	log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
 	flag.PrintDefaults()
-
 	os.Exit(1)
 }
 
@@ -33,19 +32,23 @@ func main() {
 		flag.Usage()
 	}
 
-	var tmp interface{}
-	if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
+	var decoded interface{}
+	if _, err := toml.DecodeReader(os.Stdin, &decoded); err != nil {
 		log.Fatalf("Error decoding TOML: %s", err)
 	}
 
-	typedTmp := translate(tmp)
-	if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
+	j := json.NewEncoder(os.Stdout)
+	j.SetIndent("", "  ")
+	if err := j.Encode(translate(decoded)); err != nil {
 		log.Fatalf("Error encoding JSON: %s", err)
 	}
 }
 
 func translate(tomlData interface{}) interface{} {
 	switch orig := tomlData.(type) {
+	default:
+		panic(fmt.Sprintf("Unknown type: %T", tomlData))
+
 	case map[string]interface{}:
 		typed := make(map[string]interface{}, len(orig))
 		for k, v := range orig {
@@ -63,10 +66,7 @@ func translate(tomlData interface{}) interface{} {
 		for i, v := range orig {
 			typed[i] = translate(v)
 		}
-
-		// We don't really need to tag arrays, but let's be future proof.
-		// (If TOML ever supports tuples, we'll need this.)
-		return tag("array", typed)
+		return typed
 	case time.Time:
 		return tag("datetime", orig.Format("2006-01-02T15:04:05.999999999Z07:00"))
 	case bool:
@@ -74,12 +74,13 @@ func translate(tomlData interface{}) interface{} {
 	case int64:
 		return tag("integer", fmt.Sprintf("%d", orig))
 	case float64:
+		if math.IsNaN(orig) {
+			return tag("float", "nan")
+		}
 		return tag("float", fmt.Sprintf("%v", orig))
 	case string:
 		return tag("string", orig)
 	}
-
-	panic(fmt.Sprintf("Unknown type: %T", tomlData))
 }
 
 func tag(typeName string, data interface{}) map[string]interface{} {
diff --git a/cmd/toml-test-encoder/main.go b/cmd/toml-test-encoder/main.go
index d44d4ff..ebb0497 100644
--- a/cmd/toml-test-encoder/main.go
+++ b/cmd/toml-test-encoder/main.go
@@ -1,5 +1,5 @@
-// Command toml-test-encoder satisfies the toml-test interface for testing
-// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
+// Command toml-test-encoder satisfies the toml-test interface for testing TOML
+// encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
 package main
 
 import (
@@ -16,7 +16,6 @@ import (
 
 func init() {
 	log.SetFlags(0)
-
 	flag.Usage = usage
 	flag.Parse()
 }
@@ -24,7 +23,6 @@ func init() {
 func usage() {
 	log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
 	flag.PrintDefaults()
-
 	os.Exit(1)
 }
 
@@ -38,8 +36,7 @@ func main() {
 		log.Fatalf("Error decoding JSON: %s", err)
 	}
 
-	tomlData := translate(tmp)
-	if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
+	if err := toml.NewEncoder(os.Stdout).Encode(translate(tmp)); err != nil {
 		log.Fatalf("Error encoding TOML: %s", err)
 	}
 }
@@ -56,17 +53,11 @@ func translate(typedJson interface{}) interface{} {
 		}
 		return m
 	case []interface{}:
-		tabArray := make([]map[string]interface{}, len(v))
+		a := make([]interface{}, len(v))
 		for i := range v {
-			if m, ok := translate(v[i]).(map[string]interface{}); ok {
-				tabArray[i] = m
-			} else {
-				log.Fatalf("JSON arrays may only contain objects. This " +
-					"corresponds to only tables being allowed in " +
-					"TOML table arrays.")
-			}
+			a[i] = translate(v[i])
 		}
-		return tabArray
+		return a
 	}
 	log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
 	panic("unreachable")
@@ -108,18 +99,6 @@ func untag(typed map[string]interface{}) interface{} {
 			return false
 		}
 		log.Fatalf("Could not parse '%s' as a boolean.", v)
-	case "array":
-		v := v.([]interface{})
-		array := make([]interface{}, len(v))
-		for i := range v {
-			if m, ok := v[i].(map[string]interface{}); ok {
-				array[i] = untag(m)
-			} else {
-				log.Fatalf("Arrays may only contain other arrays or "+
-					"primitive values, but found a '%T'.", m)
-			}
-		}
-		return array
 	}
 	log.Fatalf("Unrecognized tag type '%s'.", t)
 	panic("unreachable")
diff --git a/decode_test.go b/decode_test.go
index 0de8042..fb29e75 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -1031,9 +1031,10 @@ func TestDecodeErrors(t *testing.T) {
 		{"x = \n", "expected value but found '\\n' instead", true},
 
 		// Cases found by fuzzing in #155 and #239
-		{`""` + "\ufffd", "expected key separator", false},                            // used to panic with index out of range
-		{`x="""`, "unexpected EOF", true},                                             // used to hang
-		{`x = [{ key = 42 #`, "expected a comma or an inline table terminator", true}, // panic
+		{`""` + "\ufffd", "invalid UTF-8", false},
+		{`""=` + "\ufffd", "invalid UTF-8", false},
+		{`x="""`, "unexpected EOF", true},
+		{`x = [{ key = 42 #`, "expected a comma or an inline table terminator", true},
 		{`x = {a = 42 #`, "expected a comma or an inline table terminator '}', but got end of file instead", true},
 		{`x = [42 #`, "expected a comma or array terminator ']', but got end of file instead", false},
 
diff --git a/encode_test.go b/encode_test.go
index 60f2c7b..73c8cdd 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -3,6 +3,7 @@ package toml
 import (
 	"bytes"
 	"fmt"
+	"math"
 	"net"
 	"os"
 	"strings"
@@ -113,6 +114,10 @@ func TestEncode(t *testing.T) {
 			input:      struct{ String string }{"foo"},
 			wantOutput: "String = \"foo\"\n",
 		},
+		"string field with \\n escape": {
+			input:      struct{ String string }{"foo\n"},
+			wantOutput: "String = \"foo\\n\"\n",
+		},
 		"string field and unexported field": {
 			input: struct {
 				String     string
@@ -362,6 +367,21 @@ ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
 			},
 			wantOutput: "[map]\n  zero = 5\n\n  [[map.arr]]\n    friend = 5\n",
 		},
+		"empty key name": {
+			input:      map[string]int{"": 1},
+			wantOutput: `"" = 1` + "\n",
+		},
+		"key with \\n escape": {
+			input:      map[string]string{"\n": "\n"},
+			wantOutput: `"\n" = "\n"` + "\n",
+		},
+
+		"empty map name": {
+			input: map[string]interface{}{
+				"": map[string]int{"v": 1},
+			},
+			wantOutput: "[\"\"]\n  v = 1\n",
+		},
 		"(error) top-level slice": {
 			input:     []struct{ Int int }{{1}, {2}, {3}},
 			wantError: errNoKey,
@@ -378,16 +398,6 @@ ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
 			input:     map[int]string{1: ""},
 			wantError: errNonString,
 		},
-		"(error) empty key name": {
-			input:     map[string]int{"": 1},
-			wantError: errAnything,
-		},
-		"(error) empty map name": {
-			input: map[string]interface{}{
-				"": map[string]int{"v": 1},
-			},
-			wantError: errAnything,
-		},
 	}
 	for label, test := range tests {
 		encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
@@ -596,6 +606,19 @@ func TestEncodeIgnoredFields(t *testing.T) {
 	encodeExpected(t, "ignored field", value, expected, nil)
 }
 
+func TestEncodeNaN(t *testing.T) {
+	s1 := struct {
+		Nan float64 `toml:"nan"`
+		Inf float64 `toml:"inf"`
+	}{math.NaN(), math.Inf(1)}
+	s2 := struct {
+		Nan float32 `toml:"nan"`
+		Inf float32 `toml:"inf"`
+	}{float32(math.NaN()), float32(math.Inf(-1))}
+	encodeExpected(t, "", s1, "nan = nan\ninf = +inf\n", nil)
+	encodeExpected(t, "", s2, "nan = nan\ninf = -inf\n", nil)
+}
+
 func TestEncodePrimitive(t *testing.T) {
 	type MyStruct struct {
 		Data  Primitive
@@ -722,6 +745,7 @@ Fun = "why would you do this?"
 func encodeExpected(
 	t *testing.T, label string, val interface{}, wantStr string, wantErr error,
 ) {
+	t.Helper()
 	var buf bytes.Buffer
 	enc := NewEncoder(&buf)
 	err := enc.Encode(val)
@@ -739,6 +763,6 @@ func encodeExpected(
 		return
 	}
 	if got := buf.String(); wantStr != got {
-		t.Errorf("%s\nhave: %q\nwant: %q\n", label, got, wantStr)
+		t.Errorf("%s\nhave: %s\nwant: %s\n", label, got, wantStr)
 	}
 }