template_test.go 15.6 KB
Newer Older
1 2 3 4 5 6 7
// 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 (
8 9 10 11
	"bytes"
	"container/vector"
	"fmt"
	"io"
12
	"io/ioutil"
13
	"json"
14
	"os"
15
	"strings"
16
	"testing"
17 18 19
)

type Test struct {
20
	in, out, err string
21 22 23
}

type T struct {
24 25
	Item  string
	Value string
26 27
}

28
type U struct {
29
	Mp map[string]int
30 31
}

32
type S struct {
33
	Header        string
34
	HeaderPtr     *string
35
	Integer       int
36 37
	IntegerPtr    *int
	NilPtr        *int
38 39 40 41 42 43 44 45 46 47 48 49 50 51
	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
52
	Ptrmap        map[string]*string
53 54
	Iface         interface{}
	Ifaceptr      interface{}
55 56
}

57
func (s *S) PointerMethod() string { return "ptrmethod!" }
58

59
func (s S) ValueMethod() string { return "valmethod!" }
60

Russ Cox's avatar
Russ Cox committed
61 62
var t1 = T{"ItemNumber1", "ValueNumber1"}
var t2 = T{"ItemNumber2", "ValueNumber2"}
63

Russ Cox's avatar
Russ Cox committed
64
func uppercase(v interface{}) string {
65 66
	s := v.(string)
	t := ""
67
	for i := 0; i < len(s); i++ {
68
		c := s[i]
69
		if 'a' <= c && c <= 'z' {
70
			c = c + 'A' - 'a'
71
		}
72
		t += string(c)
73
	}
74
	return t
75 76
}

Russ Cox's avatar
Russ Cox committed
77
func plus1(v interface{}) string {
78 79
	i := v.(int)
	return fmt.Sprint(i + 1)
80 81
}

82 83 84 85 86 87
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]))
88
	}
Russ Cox's avatar
Russ Cox committed
89 90
}

91 92 93 94 95
func multiword(w io.Writer, format string, value ...interface{}) {
	for _, v := range value {
		fmt.Fprintf(w, "<%v>", v)
	}
}
Russ Cox's avatar
Russ Cox committed
96

Russ Cox's avatar
Russ Cox committed
97 98
var formatters = FormatterMap{
	"uppercase": writer(uppercase),
99
	"+1":        writer(plus1),
100
	"multiword": multiword,
101 102
}

Russ Cox's avatar
Russ Cox committed
103
var tests = []*Test{
104
	// Simple
Russ Cox's avatar
Russ Cox committed
105
	&Test{"", "", ""},
106
	&Test{"abc", "abc", ""},
Russ Cox's avatar
Russ Cox committed
107 108 109 110 111 112
	&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", "", ""},
113
	&Test{"\tSome Text\t\n", "\tSome Text\t\n", ""},
114
	&Test{" {.meta-right} {.meta-right} {.meta-right} \n", " } } } \n", ""},
115

Rob Pike's avatar
Rob Pike committed
116 117
	// Variables at top level
	&Test{
118
		in: "{Header}={Integer}\n",
Rob Pike's avatar
Rob Pike committed
119

Russ Cox's avatar
Russ Cox committed
120
		out: "Header=77\n",
Rob Pike's avatar
Rob Pike committed
121 122
	},

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
	&Test{
		in: "Pointers: {*HeaderPtr}={*IntegerPtr}\n",

		out: "Pointers: Header=77\n",
	},

	&Test{
		in: "Stars but not pointers: {*Header}={*Integer}\n",

		out: "Stars but not pointers: Header=77\n",
	},

	&Test{
		in: "nil pointer: {*NilPtr}={*Integer}\n",

		out: "nil pointer: <nil>=77\n",
	},

141 142
	// Method at top level
	&Test{
143
		in: "ptrmethod={PointerMethod}\n",
144 145 146 147 148

		out: "ptrmethod=ptrmethod!\n",
	},

	&Test{
149
		in: "valmethod={ValueMethod}\n",
150 151 152 153

		out: "valmethod=valmethod!\n",
	},

154 155
	// Section
	&Test{
156
		in: "{.section Data }\n" +
157
			"some text for the section\n" +
158
			"{.end}\n",
159

Russ Cox's avatar
Russ Cox committed
160
		out: "some text for the section\n",
161 162
	},
	&Test{
163 164
		in: "{.section Data }\n" +
			"{Header}={Integer}\n" +
165
			"{.end}\n",
166

Russ Cox's avatar
Russ Cox committed
167
		out: "Header=77\n",
168 169
	},
	&Test{
170 171
		in: "{.section Pdata }\n" +
			"{Header}={Integer}\n" +
172
			"{.end}\n",
173

Russ Cox's avatar
Russ Cox committed
174
		out: "Header=77\n",
175 176
	},
	&Test{
177
		in: "{.section Pdata }\n" +
178 179 180
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
181
			"{.end}\n",
182

Russ Cox's avatar
Russ Cox committed
183
		out: "data present\n",
184 185
	},
	&Test{
186
		in: "{.section Empty }\n" +
187 188 189
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
190
			"{.end}\n",
191

Russ Cox's avatar
Russ Cox committed
192
		out: "data not present\n",
193 194
	},
	&Test{
195
		in: "{.section Null }\n" +
196 197 198
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
199
			"{.end}\n",
200

Russ Cox's avatar
Russ Cox committed
201
		out: "data not present\n",
202 203
	},
	&Test{
204 205
		in: "{.section Pdata }\n" +
			"{Header}={Integer}\n" +
206
			"{.section @ }\n" +
207
			"{Header}={Integer}\n" +
208
			"{.end}\n" +
209
			"{.end}\n",
210

211
		out: "Header=77\n" +
212
			"Header=77\n",
213
	},
214

215
	&Test{
216
		in: "{.section Data}{.end} {Header}\n",
217

Russ Cox's avatar
Russ Cox committed
218
		out: " Header\n",
219
	},
220

221
	&Test{
222
		in: "{.section Integer}{@}{.end}",
223 224 225 226

		out: "77",
	},

227

228 229
	// Repeated
	&Test{
230
		in: "{.section Pdata }\n" +
231
			"{.repeated section @ }\n" +
232
			"{Item}={Value}\n" +
233
			"{.end}\n" +
234
			"{.end}\n",
235

236
		out: "ItemNumber1=ValueNumber1\n" +
237
			"ItemNumber2=ValueNumber2\n",
238
	},
Rob Pike's avatar
Rob Pike committed
239
	&Test{
240
		in: "{.section Pdata }\n" +
241
			"{.repeated section @ }\n" +
242
			"{Item}={Value}\n" +
243 244 245
			"{.or}\n" +
			"this should not appear\n" +
			"{.end}\n" +
246
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
247

248
		out: "ItemNumber1=ValueNumber1\n" +
249
			"ItemNumber2=ValueNumber2\n",
Rob Pike's avatar
Rob Pike committed
250 251
	},
	&Test{
252
		in: "{.section @ }\n" +
253 254
			"{.repeated section Empty }\n" +
			"{Item}={Value}\n" +
255 256 257
			"{.or}\n" +
			"this should appear: empty field\n" +
			"{.end}\n" +
258
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
259

Russ Cox's avatar
Russ Cox committed
260
		out: "this should appear: empty field\n",
Rob Pike's avatar
Rob Pike committed
261
	},
262
	&Test{
263 264
		in: "{.repeated section Pdata }\n" +
			"{Item}\n" +
265 266
			"{.alternates with}\n" +
			"is\nover\nmultiple\nlines\n" +
267
			"{.end}\n",
268

269 270
		out: "ItemNumber1\n" +
			"is\nover\nmultiple\nlines\n" +
271
			"ItemNumber2\n",
272
	},
273
	&Test{
274 275
		in: "{.repeated section Pdata }\n" +
			"{Item}\n" +
276 277 278 279 280 281 282 283
			"{.alternates with}\n" +
			"is\nover\nmultiple\nlines\n" +
			" {.end}\n",

		out: "ItemNumber1\n" +
			"is\nover\nmultiple\nlines\n" +
			"ItemNumber2\n",
	},
Rob Pike's avatar
Rob Pike committed
284
	&Test{
285
		in: "{.section Pdata }\n" +
286
			"{.repeated section @ }\n" +
287
			"{Item}={Value}\n" +
288 289 290 291
			"{.alternates with}DIVIDER\n" +
			"{.or}\n" +
			"this should not appear\n" +
			"{.end}\n" +
292
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
293

294 295
		out: "ItemNumber1=ValueNumber1\n" +
			"DIVIDER\n" +
296
			"ItemNumber2=ValueNumber2\n",
Rob Pike's avatar
Rob Pike committed
297
	},
298
	&Test{
299
		in: "{.repeated section Vec }\n" +
300
			"{@}\n" +
301
			"{.end}\n",
302

303
		out: "elt1\n" +
304
			"elt2\n",
305
	},
306 307
	// Same but with a space before {.end}: was a bug.
	&Test{
308
		in: "{.repeated section Vec }\n" +
309 310 311 312
			"{@} {.end}\n",

		out: "elt1 elt2 \n",
	},
313
	&Test{
314
		in: "{.repeated section Integer}{.end}",
315

316
		err: "line 1: .repeated: cannot repeat Integer (type int)",
317
	},
318

Rob Pike's avatar
Rob Pike committed
319 320
	// Nested names
	&Test{
321
		in: "{.section @ }\n" +
322
			"{InnerT.Item}={InnerT.Value}\n" +
323
			"{.end}",
Rob Pike's avatar
Rob Pike committed
324

Russ Cox's avatar
Russ Cox committed
325
		out: "ItemNumber1=ValueNumber1\n",
Rob Pike's avatar
Rob Pike committed
326
	},
Russ Cox's avatar
Russ Cox committed
327
	&Test{
328
		in: "{.section @ }\n" +
329
			"{InnerT.Item}={.section InnerT}{.section Value}{@}{.end}{.end}\n" +
330
			"{.end}",
Russ Cox's avatar
Russ Cox committed
331

Russ Cox's avatar
Russ Cox committed
332
		out: "ItemNumber1=ValueNumber1\n",
Russ Cox's avatar
Russ Cox committed
333 334
	},

Russ Cox's avatar
Russ Cox committed
335
	&Test{
336 337
		in: "{.section Emptystring}emptystring{.end}\n" +
			"{.section Header}header{.end}\n",
Russ Cox's avatar
Russ Cox committed
338

Russ Cox's avatar
Russ Cox committed
339
		out: "\nheader\n",
Russ Cox's avatar
Russ Cox committed
340
	},
Russ Cox's avatar
Russ Cox committed
341

Russ Cox's avatar
Russ Cox committed
342
	&Test{
343 344
		in: "{.section True}1{.or}2{.end}\n" +
			"{.section False}3{.or}4{.end}\n",
Russ Cox's avatar
Russ Cox committed
345

Russ Cox's avatar
Russ Cox committed
346
		out: "1\n4\n",
Russ Cox's avatar
Russ Cox committed
347
	},
348 349 350 351

	// Maps

	&Test{
352
		in: "{Mp.mapkey}\n",
353 354 355 356

		out: "Ahoy!\n",
	},
	&Test{
357
		in: "{Innermap.Mp.innerkey}\n",
358 359 360

		out: "55\n",
	},
361
	&Test{
362
		in: "{.section Innermap}{.section Mp}{innerkey}{.end}{.end}\n",
363 364 365 366

		out: "55\n",
	},
	&Test{
367
		in: "{.section JSON}{.repeated section maps}{a}{b}{.end}{.end}\n",
368 369 370

		out: "1234\n",
	},
Rob Pike's avatar
Rob Pike committed
371
	&Test{
372
		in: "{Stringmap.stringkey1}\n",
Rob Pike's avatar
Rob Pike committed
373 374 375 376

		out: "stringresult\n",
	},
	&Test{
377
		in: "{.repeated section Stringmap}\n" +
Rob Pike's avatar
Rob Pike committed
378 379 380 381 382 383
			"{@}\n" +
			"{.end}",

		out: "stringresult\n" +
			"stringresult\n",
	},
384
	&Test{
385
		in: "{.repeated section Stringmap}\n" +
386 387 388 389 390 391
			"\t{@}\n" +
			"{.end}",

		out: "\tstringresult\n" +
			"\tstringresult\n",
	},
392 393 394 395 396 397 398 399 400 401 402 403 404 405
	&Test{
		in: "{*Ptrmap.stringkey1}\n",

		out: "pointedToString\n",
	},
	&Test{
		in: "{.repeated section Ptrmap}\n" +
			"{*@}\n" +
			"{.end}",

		out: "pointedToString\n" +
			"pointedToString\n",
	},

406 407 408 409

	// Interface values

	&Test{
410
		in: "{Iface}",
411 412 413 414

		out: "[1 2 3]",
	},
	&Test{
415
		in: "{.repeated section Iface}{@}{.alternates with} {.end}",
416 417 418 419

		out: "1 2 3",
	},
	&Test{
420
		in: "{.section Iface}{@}{.end}",
421 422 423

		out: "[1 2 3]",
	},
424
	&Test{
425
		in: "{.section Ifaceptr}{Item} {Value}{.end}",
426 427 428

		out: "Item Value",
	},
429 430 431
}

func TestAll(t *testing.T) {
432 433 434 435
	// Parse
	testAll(t, func(test *Test) (*Template, os.Error) { return Parse(test.in, formatters) })
	// ParseFile
	testAll(t, func(test *Test) (*Template, os.Error) {
436 437 438 439 440
		err := ioutil.WriteFile("_test/test.tmpl", []byte(test.in), 0600)
		if err != nil {
			t.Error("unexpected write error:", err)
			return nil, err
		}
441 442
		return ParseFile("_test/test.tmpl", formatters)
	})
443 444 445 446 447 448 449 450 451 452
	// 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")
	})
453 454 455
}

func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
456
	s := new(S)
457
	// initialized by hand for clarity.
458
	s.Header = "Header"
459
	s.HeaderPtr = &s.Header
460
	s.Integer = 77
461
	s.IntegerPtr = &s.Integer
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
	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"
480 481 482 483
	s.Ptrmap = make(map[string]*string)
	x := "pointedToString"
	s.Ptrmap["stringkey1"] = &x // the same value so repeated section is order-independent
	s.Ptrmap["stringkey2"] = &x
484 485
	s.Iface = []int{1, 2, 3}
	s.Ifaceptr = &T{"Item", "Value"}
486 487

	var buf bytes.Buffer
Russ Cox's avatar
Russ Cox committed
488
	for _, test := range tests {
489
		buf.Reset()
490
		tmpl, err := parseFunc(test)
491
		if err != nil {
492
			t.Error("unexpected parse error: ", err)
493
			continue
494
		}
495
		err = tmpl.Execute(s, &buf)
496 497
		if test.err == "" {
			if err != nil {
498
				t.Error("unexpected execute error:", err)
499 500
			}
		} else {
501 502 503
			if err == nil {
				t.Errorf("expected execute error %q, got nil", test.err)
			} else if err.String() != test.err {
504
				t.Errorf("expected execute error %q, got %q", test.err, err.String())
505
			}
506
		}
507
		if buf.String() != test.out {
508
			t.Errorf("for %q: expected %q got %q", test.in, test.out, buf.String())
509 510 511 512
		}
	}
}

513
func TestMapDriverType(t *testing.T) {
514 515
	mp := map[string]string{"footer": "Ahoy!"}
	tmpl, err := Parse("template: {footer}", nil)
516 517 518
	if err != nil {
		t.Error("unexpected parse error:", err)
	}
519 520
	var b bytes.Buffer
	err = tmpl.Execute(mp, &b)
521 522 523
	if err != nil {
		t.Error("unexpected execute error:", err)
	}
524
	s := b.String()
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
	expect := "template: Ahoy!"
	if s != expect {
		t.Errorf("failed passing string as data: expected %q got %q", expect, s)
	}
}

func TestMapNoEntry(t *testing.T) {
	mp := make(map[string]int)
	tmpl, err := Parse("template: {notthere}!", 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()
	expect := "template: 0!"
	if s != expect {
		t.Errorf("failed passing string as data: expected %q got %q", expect, s)
546 547 548
	}
}

549
func TestStringDriverType(t *testing.T) {
550
	tmpl, err := Parse("template: {@}", nil)
551
	if err != nil {
552
		t.Error("unexpected parse error:", err)
553
	}
554 555
	var b bytes.Buffer
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
556
	if err != nil {
557
		t.Error("unexpected execute error:", err)
Russ Cox's avatar
Russ Cox committed
558
	}
559
	s := b.String()
560 561 562
	expect := "template: hello"
	if s != expect {
		t.Errorf("failed passing string as data: expected %q got %q", expect, s)
563 564
	}
}
Russ Cox's avatar
Russ Cox committed
565 566

func TestTwice(t *testing.T) {
567
	tmpl, err := Parse("template: {@}", nil)
Russ Cox's avatar
Russ Cox committed
568
	if err != nil {
569
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
570
	}
571 572
	var b bytes.Buffer
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
573
	if err != nil {
574
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
575
	}
576
	s := b.String()
577 578 579
	expect := "template: hello"
	if s != expect {
		t.Errorf("failed passing string as data: expected %q got %q", expect, s)
Russ Cox's avatar
Russ Cox committed
580
	}
581
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
582
	if err != nil {
583
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
584
	}
585
	s = b.String()
586 587 588
	expect += expect
	if s != expect {
		t.Errorf("failed passing string as data: expected %q got %q", expect, s)
Russ Cox's avatar
Russ Cox committed
589 590
	}
}
Rob Pike's avatar
Rob Pike committed
591 592 593 594 595

func TestCustomDelims(t *testing.T) {
	// try various lengths.  zero should catch error.
	for i := 0; i < 7; i++ {
		for j := 0; j < 7; j++ {
596
			tmpl := New(nil)
Rob Pike's avatar
Rob Pike committed
597
			// first two chars deliberately the same to test equal left and right delims
598 599 600
			ldelim := "$!#$%^&"[0:i]
			rdelim := "$*&^%$!"[0:j]
			tmpl.SetDelims(ldelim, rdelim)
Rob Pike's avatar
Rob Pike committed
601 602 603 604
			// if braces, this would be template: {@}{.meta-left}{.meta-right}
			text := "template: " +
				ldelim + "@" + rdelim +
				ldelim + ".meta-left" + rdelim +
605 606
				ldelim + ".meta-right" + rdelim
			err := tmpl.Parse(text)
Rob Pike's avatar
Rob Pike committed
607
			if err != nil {
608
				if i == 0 || j == 0 { // expected
609
					continue
Rob Pike's avatar
Rob Pike committed
610
				}
611
				t.Error("unexpected parse error:", err)
Rob Pike's avatar
Rob Pike committed
612
			} else if i == 0 || j == 0 {
613 614
				t.Errorf("expected parse error for empty delimiter: %d %d %q %q", i, j, ldelim, rdelim)
				continue
Rob Pike's avatar
Rob Pike committed
615
			}
616 617 618
			var b bytes.Buffer
			err = tmpl.Execute("hello", &b)
			s := b.String()
619
			if s != "template: hello"+ldelim+rdelim {
620
				t.Errorf("failed delim check(%q %q) %q got %q", ldelim, rdelim, text, s)
Rob Pike's avatar
Rob Pike committed
621 622 623 624
			}
		}
	}
}
Rob Pike's avatar
Rob Pike committed
625 626 627

// Test that a variable evaluates to the field itself and does not further indirection
func TestVarIndirection(t *testing.T) {
628
	s := new(S)
Rob Pike's avatar
Rob Pike committed
629
	// initialized by hand for clarity.
630
	s.InnerPointerT = &t1
Rob Pike's avatar
Rob Pike committed
631

632
	var buf bytes.Buffer
633
	input := "{.section @}{InnerPointerT}{.end}"
634
	tmpl, err := Parse(input, nil)
Rob Pike's avatar
Rob Pike committed
635
	if err != nil {
636
		t.Fatal("unexpected parse error:", err)
Rob Pike's avatar
Rob Pike committed
637
	}
638
	err = tmpl.Execute(s, &buf)
Rob Pike's avatar
Rob Pike committed
639
	if err != nil {
640
		t.Fatal("unexpected execute error:", err)
Rob Pike's avatar
Rob Pike committed
641
	}
642
	expect := fmt.Sprintf("%v", &t1) // output should be hex address of t1
643
	if buf.String() != expect {
644
		t.Errorf("for %q: expected %q got %q", input, expect, buf.String())
Rob Pike's avatar
Rob Pike committed
645 646
	}
}
647 648 649 650 651

func TestHTMLFormatterWithByte(t *testing.T) {
	s := "Test string."
	b := []byte(s)
	var buf bytes.Buffer
652
	HTMLFormatter(&buf, "", b)
653 654 655 656 657
	bs := buf.String()
	if bs != s {
		t.Errorf("munged []byte, expected: %s got: %s", s, bs)
	}
}
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679

type UF struct {
	I int
	s string
}

func TestReferenceToUnexported(t *testing.T) {
	u := &UF{3, "hello"}
	var buf bytes.Buffer
	input := "{.section @}{I}{s}{.end}"
	tmpl, err := Parse(input, nil)
	if err != nil {
		t.Fatal("unexpected parse error:", err)
	}
	err = tmpl.Execute(u, &buf)
	if err == nil {
		t.Fatal("expected execute error, got none")
	}
	if strings.Index(err.String(), "not exported") < 0 {
		t.Fatal("expected unexported error; got", err)
	}
}
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763

var formatterTests = []Test{
	{
		in: "{Header|uppercase}={Integer|+1}\n" +
			"{Header|html}={Integer|str}\n",

		out: "HEADER=78\n" +
			"Header=77\n",
	},

	{
		in: "{Header|uppercase}={Integer Header|multiword}\n" +
			"{Header|html}={Header Integer|multiword}\n" +
			"{Header|html}={Header Integer}\n",

		out: "HEADER=<77><Header>\n" +
			"Header=<Header><77>\n" +
			"Header=Header77\n",
	},
	{
		in: "{Raw}\n" +
			"{Raw|html}\n",

		out: "a <&> b\n" +
			"a &lt;&amp;&gt; b\n",
	},
	{
		in:  "{Bytes}",
		out: "hello",
	},
	{
		in:  "{Raw|uppercase|html|html}",
		out: "A &amp;lt;&amp;amp;&amp;gt; B",
	},
	{
		in:  "{Header Integer|multiword|html}",
		out: "&lt;Header&gt;&lt;77&gt;",
	},
	{
		in:  "{Integer|no_formatter|html}",
		err: `unknown formatter: "no_formatter"`,
	},
	{
		in:  "{Integer|||||}", // empty string is a valid formatter
		out: "77",
	},
}

func TestFormatters(t *testing.T) {
	data := map[string]interface{}{
		"Header":  "Header",
		"Integer": 77,
		"Raw":     "a <&> b",
		"Bytes":   []byte("hello"),
	}
	for _, c := range formatterTests {
		tmpl, err := Parse(c.in, formatters)
		if err != nil {
			if c.err == "" {
				t.Error("unexpected parse error:", err)
				continue
			}
			if strings.Index(err.String(), c.err) < 0 {
				t.Error("unexpected error: expected %q, got %q", c.err, err.String())
				continue
			}
		} else {
			if c.err != "" {
				t.Errorf("For %q, expected error, got none.", c.in)
				continue
			}
			buf := bytes.NewBuffer(nil)
			err = tmpl.Execute(data, buf)
			if err != nil {
				t.Error("unexpected Execute error: ", err)
				continue
			}
			actual := buf.String()
			if actual != c.out {
				t.Errorf("for %q: expected %q but got %q.", c.in, c.out, actual)
			}
		}
	}
}