1
0
mirror of https://github.com/golang/go synced 2024-10-02 02:18:33 -06:00

exp/template: doc and API changes suggested by rsc.

- template invocation is by string constant only.
- NewSet is gone.
- no global Funcs
- writer is now first arg to Execute

R=rsc, r
CC=golang-dev
https://golang.org/cl/4700043
This commit is contained in:
Rob Pike 2011-07-13 15:58:31 +10:00
parent 2e9388e321
commit 7aa1a1a64d
9 changed files with 84 additions and 127 deletions

View File

@ -33,13 +33,15 @@ data, defined in detail below.
is copied to the output. is copied to the output.
{{if pipeline}} T1 {{end}} {{if pipeline}} T1 {{end}}
If the value of the pipeline is the "zero value" (see below) for If the value of the pipeline is empty, no output is generated;
its type, no output is generated; otherwise, T1 is executed. otherwise, T1 is executed. The empty values are false, 0, any
nil pointer or interface value, and any array, slice, map, or
string of length zero.
Dot is unaffected. Dot is unaffected.
{{if pipeline}} T1 {{else}} T0 {{end}} {{if pipeline}} T1 {{else}} T0 {{end}}
If the value of the pipeline is the zero value for its type, T0 If the value of the pipeline is empty, T0 is executed;
is executed; otherwise, T1 is executed. Dot is unaffected. otherwise, T1 is executed. Dot is unaffected.
{{range pipeline}} T1 {{end}} {{range pipeline}} T1 {{end}}
The value of the pipeline must be an array, slice, or map. If The value of the pipeline must be an array, slice, or map. If
@ -53,29 +55,22 @@ data, defined in detail below.
T0 is executed; otherwise, dot is set to the successive elements T0 is executed; otherwise, dot is set to the successive elements
of the array, slice, or map and T1 is executed. of the array, slice, or map and T1 is executed.
{{template argument}} {{template "name"}}
If the value of the argument is a string, the template with that The template with the specified name is executed with nil data.
name is executed with nil data. If the value of arg is of type
*Template, that template is executed.
{{template argument pipeline}} {{template "name" pipeline}}
If the value of the argument is a string, the template with that The template with the specified name is executed with dot set
name is executed with data set to the value of the pipeline. If to the value of the pipeline.
the value of arg is of type *Template, that template is
executed.
{{with pipeline}} T1 {{end}} {{with pipeline}} T1 {{end}}
If the value of the pipeline is the zero value for its type, no If the value of the pipeline is empty, no output is generated;
output is generated; otherwise, dot is set to the value of the otherwise, dot is set to the value of the pipeline and T1 is
pipeline and T1 is executed. executed.
{{with pipeline}} T1 {{else}} T0 {{end}} {{with pipeline}} T1 {{else}} T0 {{end}}
If the value of the pipeline is the zero value for its type, dot If the value of the pipeline is empty, dot is unaffected and T0
is unaffected and T0 is executed; otherwise, dot is set to the is executed; otherwise, dot is set to the value of the pipeline
value of the pipeline and T1 is executed. and T1 is executed.
"Zero value" means the true zero value in Go terms. Also, for arrays, slices,
maps, and strings, any value v with len(v)==0 counts as a zero value.
Arguments Arguments
@ -106,12 +101,12 @@ An argument is a simple value, denoted by one of the following.
such as such as
.Method .Method
The result is the value of invoking the method with dot as the The result is the value of invoking the method with dot as the
receiver, dot.Method(). Such methods must have one return value (of receiver, dot.Method(). Such a method must have one return value (of
any type) or two return values, the second of which is an os.Error. any type) or two return values, the second of which is an os.Error.
If it has two and the returned error is non-nil, execution terminates If it has two and the returned error is non-nil, execution terminates
and that error is returned to the caller as the value of Execute. and an error is returned to the caller as the value of Execute.
Method invocations may be chained, but only the last element of Method invocations may be chained, but only the last element of
the chain may be a method; other others must be struct fields: the chain may be a method; others must be struct fields:
.Field1.Field2.Method .Field1.Field2.Method
Methods can also be evaluated on variables, including chaining: Methods can also be evaluated on variables, including chaining:
$x.Field1.Method $x.Field1.Method
@ -173,7 +168,7 @@ All produce the quoted word "output":
A string constant. A string constant.
{{`"output"`}} {{`"output"`}}
A raw string constant. A raw string constant.
{{printf "%q" output}} {{printf "%q" "output"}}
A function call. A function call.
{{"output" | printf "%q"}} {{"output" | printf "%q"}}
A function call whose final argument comes from the previous A function call whose final argument comes from the previous
@ -182,14 +177,12 @@ All produce the quoted word "output":
A more elaborate call. A more elaborate call.
{{"output" | printf "%s" | printf "%q"}} {{"output" | printf "%s" | printf "%q"}}
A longer chain. A longer chain.
{{$x := "output" | printf "%s" | printf "%q"}}
An unused variables captures the output.
{{with "output"}}{{printf "%q" .}}{{end}} {{with "output"}}{{printf "%q" .}}{{end}}
A with action using dot. A with action using dot.
{{with $x := "output" | printf "%q"}}{{$x}}{{end}} {{with $x := "output" | printf "%q"}}{{$x}}{{end}}
A with action creates and uses a variable. A with action that creates and uses a variable.
{{with $x := "output"}}{{printf "%q" $x}}{{end}} {{with $x := "output"}}{{printf "%q" $x}}{{end}}
A with action uses the variable in another action. A with action that uses the variable in another action.
{{with $x := "output"}}{{$x | printf "%q"}}{{end}} {{with $x := "output"}}{{$x | printf "%q"}}{{end}}
The same, but pipelined. The same, but pipelined.
@ -230,10 +223,10 @@ be true.
Template sets Template sets
All templates are named by a string specified when they are created. A template Each template is named by a string specified when it is created. A template may
may use a template invocation to instantiate another template directly or by its use a template invocation to instantiate another template directly or by its
name; see the explanation of the template action above. The name of a template name; see the explanation of the template action above. The name is looked up
is looked up in the template set active during the invocation. in the template set active during the invocation.
If no template invocation actions occur in the template, the issue of template If no template invocation actions occur in the template, the issue of template
sets can be ignored. If it does contain invocations, though, a set must be sets can be ignored. If it does contain invocations, though, a set must be
@ -241,10 +234,9 @@ defined in which to look up the names.
There are two ways to construct template sets. There are two ways to construct template sets.
The first is to use the Parse method of Set to create a set of named templates The first is to use a Set's Parse method to create a set of named templates from
by reading a single string defining multiple templates. The syntax of the a single input defining multiple templates. The syntax of the definitions is to
definitions is to surround each template declaration with a define and end surround each template declaration with a define and end action.
action; those actions are discarded after parsing.
The define action names the template being created by providing a string The define action names the template being created by providing a string
constant. Here is a simple example of input to Set.Parse: constant. Here is a simple example of input to Set.Parse:
@ -256,14 +248,14 @@ constant. Here is a simple example of input to Set.Parse:
This defines two templates, T1 and T2, and a third T3 that invokes the other two This defines two templates, T1 and T2, and a third T3 that invokes the other two
when it is executed. when it is executed.
The second way to build a template set is to use the Add method of Set to bind The second way to build a template set is to use Set's Add method to add
a template to a set. A template may be bound to multiple sets. a template to a set. A template may be bound to multiple sets.
Set.Parse may be called multiple times on different inputs to construct the set. Set.Parse may be called multiple times on different inputs to construct the set.
Two sets may therefore be constructed with a common base set of templates plus, Two sets may therefore be constructed with a common base set of templates plus,
through a second Parse call each, specializations for some elements. through a second Parse call each, specializations for some elements.
When templates are executed via Template.Execute, no set is defined and so no When a template is executed via Template.Execute, no set is defined and so no
template invocations are possible. The method Template.ExecuteInSet provides a template invocations are possible. The method Template.ExecuteInSet provides a
way to specify a template set when executing a template directly. way to specify a template set when executing a template directly.

View File

@ -220,27 +220,12 @@ func (s *state) walkRange(dot reflect.Value, r *rangeNode) {
} }
func (s *state) walkTemplate(dot reflect.Value, t *templateNode) { func (s *state) walkTemplate(dot reflect.Value, t *templateNode) {
// Can't use evalArg because there are two types we expect. if s.set == nil {
arg := s.evalEmptyInterface(dot, t.name) s.errorf("no set defined in which to invoke template named %q", t.name)
if !arg.IsValid() {
s.errorf("invalid value in template invocation; expected string or *Template")
} }
var tmpl *Template tmpl := s.set.tmpl[t.name]
if arg.Type() == reflect.TypeOf((*Template)(nil)) { if tmpl == nil {
tmpl = arg.Interface().(*Template) s.errorf("template %q not in set", t.name)
if tmpl == nil {
s.errorf("nil template")
}
} else {
s.validateType(arg, reflect.TypeOf(""))
name := arg.String()
if s.set == nil {
s.errorf("no set defined in which to invoke template named %q", name)
}
tmpl = s.set.tmpl[name]
if tmpl == nil {
s.errorf("template %q not in set", name)
}
} }
defer s.pop(s.mark()) defer s.pop(s.mark())
dot = s.evalPipeline(dot, t.pipe) dot = s.evalPipeline(dot, t.pipe)

View File

@ -448,13 +448,13 @@ func TestTree(t *testing.T) {
}, },
}, },
} }
set := NewSet() set := new(Set)
err := set.Parse(treeTemplate) err := set.Parse(treeTemplate)
if err != nil { if err != nil {
t.Fatal("parse error:", err) t.Fatal("parse error:", err)
} }
var b bytes.Buffer var b bytes.Buffer
err = set.Execute("tree", &b, tree) err = set.Execute(&b, "tree", tree)
if err != nil { if err != nil {
t.Fatal("exec error:", err) t.Fatal("exec error:", err)
} }

View File

@ -18,7 +18,7 @@ import (
// FuncMap is the type of the map defining the mapping from names to functions. // FuncMap is the type of the map defining the mapping from names to functions.
// Each function must have either a single return value, or two return values of // Each function must have either a single return value, or two return values of
// which the second has type os.Error. If the second argument evaluates to non-nil // which the second has type os.Error. If the second argument evaluates to non-nil
// during execution, execution terminates and the error is returned by Execute. // during execution, execution terminates and Execute returns an error.
type FuncMap map[string]interface{} type FuncMap map[string]interface{}
var funcs = map[string]reflect.Value{ var funcs = map[string]reflect.Value{
@ -33,13 +33,6 @@ var funcs = map[string]reflect.Value{
"println": reflect.ValueOf(fmt.Sprintln), "println": reflect.ValueOf(fmt.Sprintln),
} }
// Funcs adds to the global function map the elements of the
// argument map. It panics if a value in the map is not a function
// with appropriate return type.
func Funcs(funcMap FuncMap) {
addFuncs(funcs, funcMap)
}
// addFuncs adds to values the functions in funcs, converting them to reflect.Values. // addFuncs adds to values the functions in funcs, converting them to reflect.Values.
func addFuncs(values map[string]reflect.Value, funcMap FuncMap) { func addFuncs(values map[string]reflect.Value, funcMap FuncMap) {
for name, fn := range funcMap { for name, fn := range funcMap {

View File

@ -42,17 +42,15 @@ func (t *Template) MustParseFile(filename string) *Template {
return t return t
} }
// ParseFile is a helper function that creates a new Template and parses // ParseFile creates a new Template and parses the template definition from
// the template definition from the named file. // the named file. The template name is the base name of the file.
// The template name is the base name of the file.
func ParseFile(filename string) (*Template, os.Error) { func ParseFile(filename string) (*Template, os.Error) {
t := New(filepath.Base(filename)) t := New(filepath.Base(filename))
return t, t.ParseFile(filename) return t, t.ParseFile(filename)
} }
// MustParseFile is a helper function that creates a new Template and parses // MustParseFile creates a new Template and parses the template definition
// the template definition from the named file. // from the named file. The template name is the base name of the file.
// The template name is the base name of the file.
// It panics if the file cannot be read or the template cannot be parsed. // It panics if the file cannot be read or the template cannot be parsed.
func MustParseFile(filename string) *Template { func MustParseFile(filename string) *Template {
return New(filepath.Base(filename)).MustParseFile(filename) return New(filepath.Base(filename)).MustParseFile(filename)
@ -85,16 +83,16 @@ func (s *Set) MustParseFile(filename string) *Set {
return s return s
} }
// ParseSetFile is a helper function that creates a new Set and parses // ParseSetFile creates a new Set and parses the set definition from the
// the set definition from the named file. // named file.
func ParseSetFile(filename string) (*Set, os.Error) { func ParseSetFile(filename string) (*Set, os.Error) {
s := NewSet() s := new(Set)
return s, s.ParseFile(filename) return s, s.ParseFile(filename)
} }
// MustParseSetFile is a helper function that creates a new Set and parses // MustParseSetFile creates a new Set and parses the set definition from the
// the set definition from the named file. // named file.
// It panics if the file cannot be read or the set cannot be parsed. // It panics if the file cannot be read or the set cannot be parsed.
func MustParseSetFile(filename string) *Set { func MustParseSetFile(filename string) *Set {
return NewSet().MustParseFile(filename) return new(Set).MustParseFile(filename)
} }

View File

@ -477,19 +477,19 @@ func (r *rangeNode) String() string {
type templateNode struct { type templateNode struct {
nodeType nodeType
line int line int
name node name string
pipe *pipeNode pipe *pipeNode
} }
func newTemplate(line int, name node, pipe *pipeNode) *templateNode { func newTemplate(line int, name string, pipe *pipeNode) *templateNode {
return &templateNode{nodeType: nodeTemplate, line: line, name: name, pipe: pipe} return &templateNode{nodeType: nodeTemplate, line: line, name: name, pipe: pipe}
} }
func (t *templateNode) String() string { func (t *templateNode) String() string {
if t.pipe == nil { if t.pipe == nil {
return fmt.Sprintf("{{template %s}}", t.name) return fmt.Sprintf("{{template %q}}", t.name)
} }
return fmt.Sprintf("{{template %s %s}}", t.name, t.pipe) return fmt.Sprintf("{{template %q %s}}", t.name, t.pipe)
} }
// withNode represents a {{with}} action and its commands. // withNode represents a {{with}} action and its commands.
@ -523,9 +523,9 @@ func New(name string) *Template {
} }
} }
// Funcs adds to the template's function map the elements of the // Funcs adds the elements of the argument map to the template's function
// argument map. It panics if a value in the map is not a function // map. It panics if a value in the map is not a function with appropriate
// with appropriate return type. // return type.
// The return value is the template, so calls can be chained. // The return value is the template, so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template { func (t *Template) Funcs(funcMap FuncMap) *Template {
addFuncs(t.funcs, funcMap) addFuncs(t.funcs, funcMap)
@ -800,25 +800,14 @@ func (t *Template) elseControl() node {
// Template keyword is past. The name must be something that can evaluate // Template keyword is past. The name must be something that can evaluate
// to a string. // to a string.
func (t *Template) templateControl() node { func (t *Template) templateControl() node {
var name node var name string
switch token := t.next(); token.typ { switch token := t.next(); token.typ {
case itemIdentifier:
if _, ok := findFunction(token.val, t, t.set); !ok {
t.errorf("function %q not defined", token.val)
}
name = newIdentifier(token.val)
case itemDot:
name = newDot()
case itemVariable:
name = t.useVar(token.val)
case itemField:
name = newField(token.val)
case itemString, itemRawString: case itemString, itemRawString:
s, err := strconv.Unquote(token.val) s, err := strconv.Unquote(token.val)
if err != nil { if err != nil {
t.error(err) t.error(err)
} }
name = newString(s) name = s
default: default:
t.unexpected(token, "template invocation") t.unexpected(token, "template invocation")
} }

View File

@ -204,15 +204,13 @@ var parseTests = []parseTest{
{"constants", "{{range .SI 1 -3.2i true false 'a'}}{{end}}", noError, {"constants", "{{range .SI 1 -3.2i true false 'a'}}{{end}}", noError,
`[({{range [(command: [F=[SI] N=1 N=-3.2i B=true B=false N='a'])]}} [])]`}, `[({{range [(command: [F=[SI] N=1 N=-3.2i B=true B=false N='a'])]}} [])]`},
{"template", "{{template `x`}}", noError, {"template", "{{template `x`}}", noError,
"[{{template S=`x`}}]"}, `[{{template "x"}}]`},
{"template", "{{template `x` .Y}}", noError, {"template with arg", "{{template `x` .Y}}", noError,
"[{{template S=`x` [(command: [F=[Y]])]}}]"}, `[{{template "x" [(command: [F=[Y]])]}}]`},
{"with", "{{with .X}}hello{{end}}", noError, {"with", "{{with .X}}hello{{end}}", noError,
`[({{with [(command: [F=[X]])]}} [(text: "hello")])]`}, `[({{with [(command: [F=[X]])]}} [(text: "hello")])]`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError, {"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`[({{with [(command: [F=[X]])]}} [(text: "hello")] {{else}} [(text: "goodbye")])]`}, `[({{with [(command: [F=[X]])]}} [(text: "hello")] {{else}} [(text: "goodbye")])]`},
{"variable in template", "{{with $v := `hi`}}{{template $v}}{{end}}", noError,
"[({{with [$v] := [(command: [S=`hi`])]}} [{{template V=[$v]}}])]"},
// Errors. // Errors.
{"unclosed action", "hello{{range", hasError, ""}, {"unclosed action", "hello{{range", hasError, ""},
{"unmatched end", "{{end}}", hasError, ""}, {"unmatched end", "{{end}}", hasError, ""},
@ -223,6 +221,8 @@ var parseTests = []parseTest{
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""}, {"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
{"variable undefined in template", "{{template $v}}", hasError, ""}, {"variable undefined in template", "{{template $v}}", hasError, ""},
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""}, {"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
{"template with field ref", "{{template .X}}", hasError, ""},
{"template with var", "{{template $v}}", hasError, ""},
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {

View File

@ -14,33 +14,35 @@ import (
) )
// Set holds a set of related templates that can refer to one another by name. // Set holds a set of related templates that can refer to one another by name.
// The zero value represents an empty set.
// A template may be a member of multiple sets. // A template may be a member of multiple sets.
type Set struct { type Set struct {
tmpl map[string]*Template tmpl map[string]*Template
funcs map[string]reflect.Value funcs map[string]reflect.Value
} }
// NewSet allocates a new, empty template set. func (s *Set) init() {
func NewSet() *Set { if s.tmpl == nil {
return &Set{ s.tmpl = make(map[string]*Template)
tmpl: make(map[string]*Template), s.funcs = make(map[string]reflect.Value)
funcs: make(map[string]reflect.Value),
} }
} }
// Funcs adds to the set's function map the elements of the // Funcs adds the elements of the argument map to the set's function map. It
// argument map. It panics if a value in the map is not a function // panics if a value in the map is not a function with appropriate return
// with appropriate return type. // type.
// The return value is the set, so calls can be chained. // The return value is the set, so calls can be chained.
func (s *Set) Funcs(funcMap FuncMap) *Set { func (s *Set) Funcs(funcMap FuncMap) *Set {
s.init()
addFuncs(s.funcs, funcMap) addFuncs(s.funcs, funcMap)
return s return s
} }
// Add adds the argument templates to the set. It panics if the call // Add adds the argument templates to the set. It panics if two templates
// attempts to reuse a name defined in the set. // with the same name are added.
// The return value is the set, so calls can be chained. // The return value is the set, so calls can be chained.
func (s *Set) Add(templates ...*Template) *Set { func (s *Set) Add(templates ...*Template) *Set {
s.init()
for _, t := range templates { for _, t := range templates {
if _, ok := s.tmpl[t.name]; ok { if _, ok := s.tmpl[t.name]; ok {
panic(fmt.Errorf("template: %q already defined in set", t.name)) panic(fmt.Errorf("template: %q already defined in set", t.name))
@ -54,6 +56,7 @@ func (s *Set) Add(templates ...*Template) *Set {
// It panics if the call attempts to reuse a name defined in the set. // It panics if the call attempts to reuse a name defined in the set.
// The return value is the set, so calls can be chained. // The return value is the set, so calls can be chained.
func (s *Set) AddSet(set *Set) *Set { func (s *Set) AddSet(set *Set) *Set {
s.init()
for _, t := range set.tmpl { for _, t := range set.tmpl {
if _, ok := s.tmpl[t.name]; ok { if _, ok := s.tmpl[t.name]; ok {
panic(fmt.Errorf("template: %q already defined in set", t.name)) panic(fmt.Errorf("template: %q already defined in set", t.name))
@ -68,6 +71,7 @@ func (s *Set) AddSet(set *Set) *Set {
// template is replaced. // template is replaced.
// The return value is the set, so calls can be chained. // The return value is the set, so calls can be chained.
func (s *Set) Union(set *Set) *Set { func (s *Set) Union(set *Set) *Set {
s.init()
for _, t := range set.tmpl { for _, t := range set.tmpl {
s.tmpl[t.name] = t s.tmpl[t.name] = t
} }
@ -80,10 +84,9 @@ func (s *Set) Template(name string) *Template {
return s.tmpl[name] return s.tmpl[name]
} }
// Execute looks for the named template in the set and then applies that // Execute applies the named template to the specified data object, writing
// template to the specified data object, writing the output to wr. Nested // the output to wr. Nested template invocations will be resolved from the set.
// template invocations will be resolved from the set. func (s *Set) Execute(wr io.Writer, name string, data interface{}) os.Error {
func (s *Set) Execute(name string, wr io.Writer, data interface{}) os.Error {
tmpl := s.tmpl[name] tmpl := s.tmpl[name]
if tmpl == nil { if tmpl == nil {
return fmt.Errorf("template: no template %q in set", name) return fmt.Errorf("template: no template %q in set", name)
@ -110,6 +113,7 @@ func (s *Set) recover(errp *os.Error) {
// to the set. If a template is redefined, the element in the set is // to the set. If a template is redefined, the element in the set is
// overwritten with the new definition. // overwritten with the new definition.
func (s *Set) Parse(text string) (err os.Error) { func (s *Set) Parse(text string) (err os.Error) {
s.init()
defer s.recover(&err) defer s.recover(&err)
lex := lex("set", text) lex := lex("set", text)
const context = "define clause" const context = "define clause"

View File

@ -38,7 +38,7 @@ var setParseTests = []setParseTest{
func TestSetParse(t *testing.T) { func TestSetParse(t *testing.T) {
for _, test := range setParseTests { for _, test := range setParseTests {
set := NewSet() set := new(Set)
err := set.Parse(test.input) err := set.Parse(test.input)
switch { switch {
case err == nil && !test.ok: case err == nil && !test.ok:
@ -82,10 +82,6 @@ var setExecTests = []execTest{
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true}, {"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true}, {"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true}, {"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
{"invoke template by field", `{{template .X}}`, "TEXT", tVal, true},
{"invoke template by template", `{{template .Tmpl}}`, "test template", tVal, true},
{"invoke template by variable", `{{with $t := "x"}}{{template $t}}{{end}}`, "TEXT", tVal, true},
{"invalid: invoke template by []int", `{{template .SI}}`, "", tVal, false},
// User-defined function: test argument evaluator. // User-defined function: test argument evaluator.
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true}, {"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
@ -104,7 +100,7 @@ const setText2 = `
func TestSetExecute(t *testing.T) { func TestSetExecute(t *testing.T) {
// Declare a set with a couple of templates first. // Declare a set with a couple of templates first.
set := NewSet() set := new(Set)
err := set.Parse(setText1) err := set.Parse(setText1)
if err != nil { if err != nil {
t.Fatalf("error parsing set: %s", err) t.Fatalf("error parsing set: %s", err)