| // Copyright 2009 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package template |
| |
| import ( |
| "bytes" |
| "container/vector" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "json" |
| "os" |
| "testing" |
| ) |
| |
| type Test struct { |
| in, out, err string |
| } |
| |
| type T struct { |
| item string |
| value string |
| } |
| |
| type U struct { |
| mp map[string]int |
| } |
| |
| type S struct { |
| header string |
| integer int |
| raw string |
| innerT T |
| innerPointerT *T |
| data []T |
| pdata []*T |
| empty []*T |
| emptystring string |
| null []*T |
| vec *vector.Vector |
| true bool |
| false bool |
| mp map[string]string |
| json interface{} |
| innermap U |
| stringmap map[string]string |
| bytes []byte |
| iface interface{} |
| ifaceptr interface{} |
| } |
| |
| func (s *S) pointerMethod() string { return "ptrmethod!" } |
| |
| func (s S) valueMethod() string { return "valmethod!" } |
| |
| var t1 = T{"ItemNumber1", "ValueNumber1"} |
| var t2 = T{"ItemNumber2", "ValueNumber2"} |
| |
| func uppercase(v interface{}) string { |
| s := v.(string) |
| t := "" |
| for i := 0; i < len(s); i++ { |
| c := s[i] |
| if 'a' <= c && c <= 'z' { |
| c = c + 'A' - 'a' |
| } |
| t += string(c) |
| } |
| return t |
| } |
| |
| func plus1(v interface{}) string { |
| i := v.(int) |
| return fmt.Sprint(i + 1) |
| } |
| |
| func writer(f func(interface{}) string) func(io.Writer, string, ...interface{}) { |
| return func(w io.Writer, format string, v ...interface{}) { |
| if len(v) != 1 { |
| panic("test writer expected one arg") |
| } |
| io.WriteString(w, f(v[0])) |
| } |
| } |
| |
| func multiword(w io.Writer, format string, value ...interface{}) { |
| for _, v := range value { |
| fmt.Fprintf(w, "<%v>", v) |
| } |
| } |
| |
| var formatters = FormatterMap{ |
| "uppercase": writer(uppercase), |
| "+1": writer(plus1), |
| "multiword": multiword, |
| } |
| |
| var tests = []*Test{ |
| // Simple |
| &Test{"", "", ""}, |
| &Test{"abc", "abc", ""}, |
| &Test{"abc\ndef\n", "abc\ndef\n", ""}, |
| &Test{" {.meta-left} \n", "{", ""}, |
| &Test{" {.meta-right} \n", "}", ""}, |
| &Test{" {.space} \n", " ", ""}, |
| &Test{" {.tab} \n", "\t", ""}, |
| &Test{" {#comment} \n", "", ""}, |
| &Test{"\tSome Text\t\n", "\tSome Text\t\n", ""}, |
| &Test{" {.meta-right} {.meta-right} {.meta-right} \n", " } } } \n", ""}, |
| |
| // Variables at top level |
| &Test{ |
| in: "{header}={integer}\n", |
| |
| out: "Header=77\n", |
| }, |
| |
| // Method at top level |
| &Test{ |
| in: "ptrmethod={pointerMethod}\n", |
| |
| out: "ptrmethod=ptrmethod!\n", |
| }, |
| |
| &Test{ |
| in: "valmethod={valueMethod}\n", |
| |
| out: "valmethod=valmethod!\n", |
| }, |
| |
| // Section |
| &Test{ |
| in: "{.section data }\n" + |
| "some text for the section\n" + |
| "{.end}\n", |
| |
| out: "some text for the section\n", |
| }, |
| &Test{ |
| in: "{.section data }\n" + |
| "{header}={integer}\n" + |
| "{.end}\n", |
| |
| out: "Header=77\n", |
| }, |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{header}={integer}\n" + |
| "{.end}\n", |
| |
| out: "Header=77\n", |
| }, |
| &Test{ |
| in: "{.section pdata }\n" + |
| "data present\n" + |
| "{.or}\n" + |
| "data not present\n" + |
| "{.end}\n", |
| |
| out: "data present\n", |
| }, |
| &Test{ |
| in: "{.section empty }\n" + |
| "data present\n" + |
| "{.or}\n" + |
| "data not present\n" + |
| "{.end}\n", |
| |
| out: "data not present\n", |
| }, |
| &Test{ |
| in: "{.section null }\n" + |
| "data present\n" + |
| "{.or}\n" + |
| "data not present\n" + |
| "{.end}\n", |
| |
| out: "data not present\n", |
| }, |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{header}={integer}\n" + |
| "{.section @ }\n" + |
| "{header}={integer}\n" + |
| "{.end}\n" + |
| "{.end}\n", |
| |
| out: "Header=77\n" + |
| "Header=77\n", |
| }, |
| |
| &Test{ |
| in: "{.section data}{.end} {header}\n", |
| |
| out: " Header\n", |
| }, |
| |
| &Test{ |
| in: "{.section integer}{@}{.end}", |
| |
| out: "77", |
| }, |
| |
| // Repeated |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{.repeated section @ }\n" + |
| "{item}={value}\n" + |
| "{.end}\n" + |
| "{.end}\n", |
| |
| out: "ItemNumber1=ValueNumber1\n" + |
| "ItemNumber2=ValueNumber2\n", |
| }, |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{.repeated section @ }\n" + |
| "{item}={value}\n" + |
| "{.or}\n" + |
| "this should not appear\n" + |
| "{.end}\n" + |
| "{.end}\n", |
| |
| out: "ItemNumber1=ValueNumber1\n" + |
| "ItemNumber2=ValueNumber2\n", |
| }, |
| &Test{ |
| in: "{.section @ }\n" + |
| "{.repeated section empty }\n" + |
| "{item}={value}\n" + |
| "{.or}\n" + |
| "this should appear: empty field\n" + |
| "{.end}\n" + |
| "{.end}\n", |
| |
| out: "this should appear: empty field\n", |
| }, |
| &Test{ |
| in: "{.repeated section pdata }\n" + |
| "{item}\n" + |
| "{.alternates with}\n" + |
| "is\nover\nmultiple\nlines\n" + |
| "{.end}\n", |
| |
| out: "ItemNumber1\n" + |
| "is\nover\nmultiple\nlines\n" + |
| "ItemNumber2\n", |
| }, |
| &Test{ |
| in: "{.repeated section pdata }\n" + |
| "{item}\n" + |
| "{.alternates with}\n" + |
| "is\nover\nmultiple\nlines\n" + |
| " {.end}\n", |
| |
| out: "ItemNumber1\n" + |
| "is\nover\nmultiple\nlines\n" + |
| "ItemNumber2\n", |
| }, |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{.repeated section @ }\n" + |
| "{item}={value}\n" + |
| "{.alternates with}DIVIDER\n" + |
| "{.or}\n" + |
| "this should not appear\n" + |
| "{.end}\n" + |
| "{.end}\n", |
| |
| out: "ItemNumber1=ValueNumber1\n" + |
| "DIVIDER\n" + |
| "ItemNumber2=ValueNumber2\n", |
| }, |
| &Test{ |
| in: "{.repeated section vec }\n" + |
| "{@}\n" + |
| "{.end}\n", |
| |
| out: "elt1\n" + |
| "elt2\n", |
| }, |
| // Same but with a space before {.end}: was a bug. |
| &Test{ |
| in: "{.repeated section vec }\n" + |
| "{@} {.end}\n", |
| |
| out: "elt1 elt2 \n", |
| }, |
| &Test{ |
| in: "{.repeated section integer}{.end}", |
| |
| err: "line 1: .repeated: cannot repeat integer (type int)", |
| }, |
| |
| // Nested names |
| &Test{ |
| in: "{.section @ }\n" + |
| "{innerT.item}={innerT.value}\n" + |
| "{.end}", |
| |
| out: "ItemNumber1=ValueNumber1\n", |
| }, |
| &Test{ |
| in: "{.section @ }\n" + |
| "{innerT.item}={.section innerT}{.section value}{@}{.end}{.end}\n" + |
| "{.end}", |
| |
| out: "ItemNumber1=ValueNumber1\n", |
| }, |
| |
| |
| // Formatters |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{header|uppercase}={integer|+1}\n" + |
| "{header|html}={integer|str}\n" + |
| "{.end}\n", |
| |
| out: "HEADER=78\n" + |
| "Header=77\n", |
| }, |
| |
| &Test{ |
| in: "{.section pdata }\n" + |
| "{header|uppercase}={integer header|multiword}\n" + |
| "{header|html}={header integer|multiword}\n" + |
| "{header|html}={header integer}\n" + |
| "{.end}\n", |
| |
| out: "HEADER=<77><Header>\n" + |
| "Header=<Header><77>\n" + |
| "Header=Header77\n", |
| }, |
| |
| &Test{ |
| in: "{raw}\n" + |
| "{raw|html}\n", |
| |
| out: "&<>!@ #$%^\n" + |
| "&<>!@ #$%^\n", |
| }, |
| |
| &Test{ |
| in: "{.section emptystring}emptystring{.end}\n" + |
| "{.section header}header{.end}\n", |
| |
| out: "\nheader\n", |
| }, |
| |
| &Test{ |
| in: "{.section true}1{.or}2{.end}\n" + |
| "{.section false}3{.or}4{.end}\n", |
| |
| out: "1\n4\n", |
| }, |
| |
| &Test{ |
| in: "{bytes}", |
| |
| out: "hello", |
| }, |
| |
| // Maps |
| |
| &Test{ |
| in: "{mp.mapkey}\n", |
| |
| out: "Ahoy!\n", |
| }, |
| &Test{ |
| in: "{innermap.mp.innerkey}\n", |
| |
| out: "55\n", |
| }, |
| &Test{ |
| in: "{.section innermap}{.section mp}{innerkey}{.end}{.end}\n", |
| |
| out: "55\n", |
| }, |
| &Test{ |
| in: "{.section json}{.repeated section maps}{a}{b}{.end}{.end}\n", |
| |
| out: "1234\n", |
| }, |
| &Test{ |
| in: "{stringmap.stringkey1}\n", |
| |
| out: "stringresult\n", |
| }, |
| &Test{ |
| in: "{.repeated section stringmap}\n" + |
| "{@}\n" + |
| "{.end}", |
| |
| out: "stringresult\n" + |
| "stringresult\n", |
| }, |
| &Test{ |
| in: "{.repeated section stringmap}\n" + |
| "\t{@}\n" + |
| "{.end}", |
| |
| out: "\tstringresult\n" + |
| "\tstringresult\n", |
| }, |
| |
| // Interface values |
| |
| &Test{ |
| in: "{iface}", |
| |
| out: "[1 2 3]", |
| }, |
| &Test{ |
| in: "{.repeated section iface}{@}{.alternates with} {.end}", |
| |
| out: "1 2 3", |
| }, |
| &Test{ |
| in: "{.section iface}{@}{.end}", |
| |
| out: "[1 2 3]", |
| }, |
| &Test{ |
| in: "{.section ifaceptr}{item} {value}{.end}", |
| |
| out: "Item Value", |
| }, |
| } |
| |
| func TestAll(t *testing.T) { |
| // Parse |
| testAll(t, func(test *Test) (*Template, os.Error) { return Parse(test.in, formatters) }) |
| // ParseFile |
| testAll(t, func(test *Test) (*Template, os.Error) { |
| err := ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600) |
| if err != nil { |
| t.Error("unexpected write error:", err) |
| return nil, err |
| } |
| return ParseFile("_test/test.tmpl", formatters) |
| }) |
| // tmpl.ParseFile |
| testAll(t, func(test *Test) (*Template, os.Error) { |
| err := ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600) |
| if err != nil { |
| t.Error("unexpected write error:", err) |
| return nil, err |
| } |
| tmpl := New(formatters) |
| return tmpl, tmpl.ParseFile("_test/test.tmpl") |
| }) |
| } |
| |
| func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) { |
| s := new(S) |
| // initialized by hand for clarity. |
| s.header = "Header" |
| s.integer = 77 |
| s.raw = "&<>!@ #$%^" |
| s.innerT = t1 |
| s.data = []T{t1, t2} |
| s.pdata = []*T{&t1, &t2} |
| s.empty = []*T{} |
| s.null = nil |
| s.vec = new(vector.Vector) |
| s.vec.Push("elt1") |
| s.vec.Push("elt2") |
| s.true = true |
| s.false = false |
| s.mp = make(map[string]string) |
| s.mp["mapkey"] = "Ahoy!" |
| json.Unmarshal([]byte(`{"maps":[{"a":1,"b":2},{"a":3,"b":4}]}`), &s.json) |
| s.innermap.mp = make(map[string]int) |
| s.innermap.mp["innerkey"] = 55 |
| s.stringmap = make(map[string]string) |
| s.stringmap["stringkey1"] = "stringresult" // the same value so repeated section is order-independent |
| s.stringmap["stringkey2"] = "stringresult" |
| s.bytes = []byte("hello") |
| s.iface = []int{1, 2, 3} |
| s.ifaceptr = &T{"Item", "Value"} |
| |
| var buf bytes.Buffer |
| for _, test := range tests { |
| buf.Reset() |
| tmpl, err := parseFunc(test) |
| if err != nil { |
| t.Error("unexpected parse error: ", err) |
| continue |
| } |
| err = tmpl.Execute(s, &buf) |
| if test.err == "" { |
| if err != nil { |
| t.Error("unexpected execute error:", err) |
| } |
| } else { |
| if err == nil { |
| t.Errorf("expected execute error %q, got nil", test.err) |
| } else if err.String() != test.err { |
| t.Errorf("expected execute error %q, got %q", test.err, err.String()) |
| } |
| } |
| if buf.String() != test.out { |
| t.Errorf("for %q: expected %q got %q", test.in, test.out, buf.String()) |
| } |
| } |
| } |
| |
| func TestMapDriverType(t *testing.T) { |
| mp := map[string]string{"footer": "Ahoy!"} |
| tmpl, err := Parse("template: {footer}", nil) |
| if err != nil { |
| t.Error("unexpected parse error:", err) |
| } |
| var b bytes.Buffer |
| err = tmpl.Execute(mp, &b) |
| if err != nil { |
| t.Error("unexpected execute error:", err) |
| } |
| s := b.String() |
| expected := "template: Ahoy!" |
| if s != expected { |
| t.Errorf("failed passing string as data: expected %q got %q", "template: Ahoy!", s) |
| } |
| } |
| |
| func TestStringDriverType(t *testing.T) { |
| tmpl, err := Parse("template: {@}", nil) |
| if err != nil { |
| t.Error("unexpected parse error:", err) |
| } |
| var b bytes.Buffer |
| err = tmpl.Execute("hello", &b) |
| if err != nil { |
| t.Error("unexpected execute error:", err) |
| } |
| s := b.String() |
| if s != "template: hello" { |
| t.Errorf("failed passing string as data: expected %q got %q", "template: hello", s) |
| } |
| } |
| |
| func TestTwice(t *testing.T) { |
| tmpl, err := Parse("template: {@}", nil) |
| if err != nil { |
| t.Error("unexpected parse error:", err) |
| } |
| var b bytes.Buffer |
| err = tmpl.Execute("hello", &b) |
| if err != nil { |
| t.Error("unexpected parse error:", err) |
| } |
| s := b.String() |
| text := "template: hello" |
| if s != text { |
| t.Errorf("failed passing string as data: expected %q got %q", text, s) |
| } |
| err = tmpl.Execute("hello", &b) |
| if err != nil { |
| t.Error("unexpected parse error:", err) |
| } |
| s = b.String() |
| text += text |
| if s != text { |
| t.Errorf("failed passing string as data: expected %q got %q", text, s) |
| } |
| } |
| |
| func TestCustomDelims(t *testing.T) { |
| // try various lengths. zero should catch error. |
| for i := 0; i < 7; i++ { |
| for j := 0; j < 7; j++ { |
| tmpl := New(nil) |
| // first two chars deliberately the same to test equal left and right delims |
| ldelim := "$!#$%^&"[0:i] |
| rdelim := "$*&^%$!"[0:j] |
| tmpl.SetDelims(ldelim, rdelim) |
| // if braces, this would be template: {@}{.meta-left}{.meta-right} |
| text := "template: " + |
| ldelim + "@" + rdelim + |
| ldelim + ".meta-left" + rdelim + |
| ldelim + ".meta-right" + rdelim |
| err := tmpl.Parse(text) |
| if err != nil { |
| if i == 0 || j == 0 { // expected |
| continue |
| } |
| t.Error("unexpected parse error:", err) |
| } else if i == 0 || j == 0 { |
| t.Errorf("expected parse error for empty delimiter: %d %d %q %q", i, j, ldelim, rdelim) |
| continue |
| } |
| var b bytes.Buffer |
| err = tmpl.Execute("hello", &b) |
| s := b.String() |
| if s != "template: hello"+ldelim+rdelim { |
| t.Errorf("failed delim check(%q %q) %q got %q", ldelim, rdelim, text, s) |
| } |
| } |
| } |
| } |
| |
| // Test that a variable evaluates to the field itself and does not further indirection |
| func TestVarIndirection(t *testing.T) { |
| s := new(S) |
| // initialized by hand for clarity. |
| s.innerPointerT = &t1 |
| |
| var buf bytes.Buffer |
| input := "{.section @}{innerPointerT}{.end}" |
| tmpl, err := Parse(input, nil) |
| if err != nil { |
| t.Fatal("unexpected parse error:", err) |
| } |
| err = tmpl.Execute(s, &buf) |
| if err != nil { |
| t.Fatal("unexpected execute error:", err) |
| } |
| expect := fmt.Sprintf("%v", &t1) // output should be hex address of t1 |
| if buf.String() != expect { |
| t.Errorf("for %q: expected %q got %q", input, expect, buf.String()) |
| } |
| } |
| |
| func TestHTMLFormatterWithByte(t *testing.T) { |
| s := "Test string." |
| b := []byte(s) |
| var buf bytes.Buffer |
| HTMLFormatter(&buf, "", b) |
| bs := buf.String() |
| if bs != s { |
| t.Errorf("munged []byte, expected: %s got: %s", s, bs) |
| } |
| } |