template_test.go 12.8 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
	"testing"
16 17 18
)

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

type T struct {
23 24
	item  string
	value string
25 26
}

27
type U struct {
28
	mp map[string]int
29 30
}

31
type S struct {
32 33 34 35 36 37 38 39 40 41 42 43 44 45
	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
46
	json          interface{}
47
	innermap      U
Rob Pike's avatar
Rob Pike committed
48
	stringmap     map[string]string
49
	bytes         []byte
50
	iface         interface{}
51
	ifaceptr      interface{}
52 53
}

54 55 56 57
func (s *S) pointerMethod() string { return "ptrmethod!" }

func (s S) valueMethod() string { return "valmethod!" }

Russ Cox's avatar
Russ Cox committed
58 59
var t1 = T{"ItemNumber1", "ValueNumber1"}
var t2 = T{"ItemNumber2", "ValueNumber2"}
60

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

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

79 80 81 82 83 84
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]))
85
	}
Russ Cox's avatar
Russ Cox committed
86 87
}

88 89 90 91 92
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
93

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

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

Rob Pike's avatar
Rob Pike committed
113 114
	// Variables at top level
	&Test{
115
		in: "{header}={integer}\n",
Rob Pike's avatar
Rob Pike committed
116

Russ Cox's avatar
Russ Cox committed
117
		out: "Header=77\n",
Rob Pike's avatar
Rob Pike committed
118 119
	},

120 121 122 123 124 125 126 127 128 129 130 131 132
	// Method at top level
	&Test{
		in: "ptrmethod={pointerMethod}\n",

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

	&Test{
		in: "valmethod={valueMethod}\n",

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

133 134
	// Section
	&Test{
135 136
		in: "{.section data }\n" +
			"some text for the section\n" +
137
			"{.end}\n",
138

Russ Cox's avatar
Russ Cox committed
139
		out: "some text for the section\n",
140 141
	},
	&Test{
142 143
		in: "{.section data }\n" +
			"{header}={integer}\n" +
144
			"{.end}\n",
145

Russ Cox's avatar
Russ Cox committed
146
		out: "Header=77\n",
147 148
	},
	&Test{
149 150
		in: "{.section pdata }\n" +
			"{header}={integer}\n" +
151
			"{.end}\n",
152

Russ Cox's avatar
Russ Cox committed
153
		out: "Header=77\n",
154 155
	},
	&Test{
156 157 158 159
		in: "{.section pdata }\n" +
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
160
			"{.end}\n",
161

Russ Cox's avatar
Russ Cox committed
162
		out: "data present\n",
163 164
	},
	&Test{
165 166 167 168
		in: "{.section empty }\n" +
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
169
			"{.end}\n",
170

Russ Cox's avatar
Russ Cox committed
171
		out: "data not present\n",
172 173
	},
	&Test{
174 175 176 177
		in: "{.section null }\n" +
			"data present\n" +
			"{.or}\n" +
			"data not present\n" +
178
			"{.end}\n",
179

Russ Cox's avatar
Russ Cox committed
180
		out: "data not present\n",
181 182
	},
	&Test{
183 184 185 186 187
		in: "{.section pdata }\n" +
			"{header}={integer}\n" +
			"{.section @ }\n" +
			"{header}={integer}\n" +
			"{.end}\n" +
188
			"{.end}\n",
189

190
		out: "Header=77\n" +
191
			"Header=77\n",
192
	},
193

194
	&Test{
195
		in: "{.section data}{.end} {header}\n",
196

Russ Cox's avatar
Russ Cox committed
197
		out: " Header\n",
198
	},
199 200 201

	// Repeated
	&Test{
202 203 204 205
		in: "{.section pdata }\n" +
			"{.repeated section @ }\n" +
			"{item}={value}\n" +
			"{.end}\n" +
206
			"{.end}\n",
207

208
		out: "ItemNumber1=ValueNumber1\n" +
209
			"ItemNumber2=ValueNumber2\n",
210
	},
Rob Pike's avatar
Rob Pike committed
211
	&Test{
212 213 214 215 216 217
		in: "{.section pdata }\n" +
			"{.repeated section @ }\n" +
			"{item}={value}\n" +
			"{.or}\n" +
			"this should not appear\n" +
			"{.end}\n" +
218
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
219

220
		out: "ItemNumber1=ValueNumber1\n" +
221
			"ItemNumber2=ValueNumber2\n",
Rob Pike's avatar
Rob Pike committed
222 223
	},
	&Test{
224 225 226 227 228 229
		in: "{.section @ }\n" +
			"{.repeated section empty }\n" +
			"{item}={value}\n" +
			"{.or}\n" +
			"this should appear: empty field\n" +
			"{.end}\n" +
230
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
231

Russ Cox's avatar
Russ Cox committed
232
		out: "this should appear: empty field\n",
Rob Pike's avatar
Rob Pike committed
233
	},
234
	&Test{
235 236 237 238
		in: "{.repeated section pdata }\n" +
			"{item}\n" +
			"{.alternates with}\n" +
			"is\nover\nmultiple\nlines\n" +
239
			"{.end}\n",
240

241 242
		out: "ItemNumber1\n" +
			"is\nover\nmultiple\nlines\n" +
243
			"ItemNumber2\n",
244
	},
245 246 247 248 249 250 251 252 253 254 255
	&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",
	},
Rob Pike's avatar
Rob Pike committed
256
	&Test{
257 258 259 260 261 262 263
		in: "{.section pdata }\n" +
			"{.repeated section @ }\n" +
			"{item}={value}\n" +
			"{.alternates with}DIVIDER\n" +
			"{.or}\n" +
			"this should not appear\n" +
			"{.end}\n" +
264
			"{.end}\n",
Rob Pike's avatar
Rob Pike committed
265

266 267
		out: "ItemNumber1=ValueNumber1\n" +
			"DIVIDER\n" +
268
			"ItemNumber2=ValueNumber2\n",
Rob Pike's avatar
Rob Pike committed
269
	},
270
	&Test{
271 272
		in: "{.repeated section vec }\n" +
			"{@}\n" +
273
			"{.end}\n",
274

275
		out: "elt1\n" +
276
			"elt2\n",
277
	},
278 279 280 281 282 283 284
	// Same but with a space before {.end}: was a bug.
	&Test{
		in: "{.repeated section vec }\n" +
			"{@} {.end}\n",

		out: "elt1 elt2 \n",
	},
285 286 287
	&Test{
		in: "{.repeated section integer}{.end}",

Rob Pike's avatar
Rob Pike committed
288
		err: "line 1: .repeated: cannot repeat integer (type int)",
289
	},
290

Rob Pike's avatar
Rob Pike committed
291 292
	// Nested names
	&Test{
293 294
		in: "{.section @ }\n" +
			"{innerT.item}={innerT.value}\n" +
295
			"{.end}",
Rob Pike's avatar
Rob Pike committed
296

Russ Cox's avatar
Russ Cox committed
297
		out: "ItemNumber1=ValueNumber1\n",
Rob Pike's avatar
Rob Pike committed
298
	},
Russ Cox's avatar
Russ Cox committed
299
	&Test{
300 301
		in: "{.section @ }\n" +
			"{innerT.item}={.section innerT}{.section value}{@}{.end}{.end}\n" +
302
			"{.end}",
Russ Cox's avatar
Russ Cox committed
303

Russ Cox's avatar
Russ Cox committed
304
		out: "ItemNumber1=ValueNumber1\n",
Russ Cox's avatar
Russ Cox committed
305 306
	},

Rob Pike's avatar
Rob Pike committed
307

308 309
	// Formatters
	&Test{
310 311 312
		in: "{.section pdata }\n" +
			"{header|uppercase}={integer|+1}\n" +
			"{header|html}={integer|str}\n" +
313
			"{.end}\n",
314

315
		out: "HEADER=78\n" +
316
			"Header=77\n",
317
	},
Russ Cox's avatar
Russ Cox committed
318

319 320 321 322 323 324 325 326 327 328 329 330
	&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",
	},

Russ Cox's avatar
Russ Cox committed
331
	&Test{
332
		in: "{raw}\n" +
333
			"{raw|html}\n",
Russ Cox's avatar
Russ Cox committed
334

335
		out: "&<>!@ #$%^\n" +
336
			"&amp;&lt;&gt;!@ #$%^\n",
Russ Cox's avatar
Russ Cox committed
337
	},
Russ Cox's avatar
Russ Cox committed
338 339

	&Test{
340
		in: "{.section emptystring}emptystring{.end}\n" +
341
			"{.section header}header{.end}\n",
Russ Cox's avatar
Russ Cox committed
342

Russ Cox's avatar
Russ Cox committed
343
		out: "\nheader\n",
Russ Cox's avatar
Russ Cox committed
344
	},
Russ Cox's avatar
Russ Cox committed
345

Russ Cox's avatar
Russ Cox committed
346
	&Test{
347
		in: "{.section true}1{.or}2{.end}\n" +
348
			"{.section false}3{.or}4{.end}\n",
Russ Cox's avatar
Russ Cox committed
349

Russ Cox's avatar
Russ Cox committed
350
		out: "1\n4\n",
Russ Cox's avatar
Russ Cox committed
351
	},
352

Russ Cox's avatar
Russ Cox committed
353 354 355 356 357 358
	&Test{
		in: "{bytes}",

		out: "hello",
	},

359 360 361 362 363 364 365 366 367 368 369 370
	// Maps

	&Test{
		in: "{mp.mapkey}\n",

		out: "Ahoy!\n",
	},
	&Test{
		in: "{innermap.mp.innerkey}\n",

		out: "55\n",
	},
371 372 373 374 375 376 377 378 379 380
	&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",
	},
Rob Pike's avatar
Rob Pike committed
381 382 383 384 385 386 387 388 389 390 391 392 393
	&Test{
		in: "{stringmap.stringkey1}\n",

		out: "stringresult\n",
	},
	&Test{
		in: "{.repeated section stringmap}\n" +
			"{@}\n" +
			"{.end}",

		out: "stringresult\n" +
			"stringresult\n",
	},
394 395 396 397 398 399 400 401
	&Test{
		in: "{.repeated section stringmap}\n" +
			"\t{@}\n" +
			"{.end}",

		out: "\tstringresult\n" +
			"\tstringresult\n",
	},
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419

	// 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]",
	},
420 421 422 423 424
	&Test{
		in: "{.section ifaceptr}{item} {value}{.end}",

		out: "Item Value",
	},
425 426 427
}

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

func testAll(t *testing.T, parseFunc func(*Test) (*Template, os.Error)) {
452
	s := new(S)
453
	// initialized by hand for clarity.
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
	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!"
469
	json.Unmarshal([]byte(`{"maps":[{"a":1,"b":2},{"a":3,"b":4}]}`), &s.json)
470 471
	s.innermap.mp = make(map[string]int)
	s.innermap.mp["innerkey"] = 55
Rob Pike's avatar
Rob Pike committed
472 473 474
	s.stringmap = make(map[string]string)
	s.stringmap["stringkey1"] = "stringresult" // the same value so repeated section is order-independent
	s.stringmap["stringkey2"] = "stringresult"
Russ Cox's avatar
Russ Cox committed
475
	s.bytes = []byte("hello")
476
	s.iface = []int{1, 2, 3}
477
	s.ifaceptr = &T{"Item", "Value"}
478 479

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

505
func TestMapDriverType(t *testing.T) {
506 507
	mp := map[string]string{"footer": "Ahoy!"}
	tmpl, err := Parse("template: {footer}", nil)
508 509 510
	if err != nil {
		t.Error("unexpected parse error:", err)
	}
511 512
	var b bytes.Buffer
	err = tmpl.Execute(mp, &b)
513 514 515
	if err != nil {
		t.Error("unexpected execute error:", err)
	}
516 517
	s := b.String()
	expected := "template: Ahoy!"
518 519 520 521 522
	if s != expected {
		t.Errorf("failed passing string as data: expected %q got %q", "template: Ahoy!", s)
	}
}

523
func TestStringDriverType(t *testing.T) {
524
	tmpl, err := Parse("template: {@}", nil)
525
	if err != nil {
526
		t.Error("unexpected parse error:", err)
527
	}
528 529
	var b bytes.Buffer
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
530
	if err != nil {
531
		t.Error("unexpected execute error:", err)
Russ Cox's avatar
Russ Cox committed
532
	}
533
	s := b.String()
534
	if s != "template: hello" {
535
		t.Errorf("failed passing string as data: expected %q got %q", "template: hello", s)
536 537
	}
}
Russ Cox's avatar
Russ Cox committed
538 539

func TestTwice(t *testing.T) {
540
	tmpl, err := Parse("template: {@}", nil)
Russ Cox's avatar
Russ Cox committed
541
	if err != nil {
542
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
543
	}
544 545
	var b bytes.Buffer
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
546
	if err != nil {
547
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
548
	}
549 550
	s := b.String()
	text := "template: hello"
Russ Cox's avatar
Russ Cox committed
551
	if s != text {
552
		t.Errorf("failed passing string as data: expected %q got %q", text, s)
Russ Cox's avatar
Russ Cox committed
553
	}
554
	err = tmpl.Execute("hello", &b)
Russ Cox's avatar
Russ Cox committed
555
	if err != nil {
556
		t.Error("unexpected parse error:", err)
Russ Cox's avatar
Russ Cox committed
557
	}
558 559
	s = b.String()
	text += text
Russ Cox's avatar
Russ Cox committed
560
	if s != text {
561
		t.Errorf("failed passing string as data: expected %q got %q", text, s)
Russ Cox's avatar
Russ Cox committed
562 563
	}
}
Rob Pike's avatar
Rob Pike committed
564 565 566 567 568

func TestCustomDelims(t *testing.T) {
	// try various lengths.  zero should catch error.
	for i := 0; i < 7; i++ {
		for j := 0; j < 7; j++ {
569
			tmpl := New(nil)
Rob Pike's avatar
Rob Pike committed
570
			// first two chars deliberately the same to test equal left and right delims
571 572 573
			ldelim := "$!#$%^&"[0:i]
			rdelim := "$*&^%$!"[0:j]
			tmpl.SetDelims(ldelim, rdelim)
Rob Pike's avatar
Rob Pike committed
574 575 576 577
			// if braces, this would be template: {@}{.meta-left}{.meta-right}
			text := "template: " +
				ldelim + "@" + rdelim +
				ldelim + ".meta-left" + rdelim +
578 579
				ldelim + ".meta-right" + rdelim
			err := tmpl.Parse(text)
Rob Pike's avatar
Rob Pike committed
580
			if err != nil {
581
				if i == 0 || j == 0 { // expected
582
					continue
Rob Pike's avatar
Rob Pike committed
583
				}
584
				t.Error("unexpected parse error:", err)
Rob Pike's avatar
Rob Pike committed
585
			} else if i == 0 || j == 0 {
586 587
				t.Errorf("expected parse error for empty delimiter: %d %d %q %q", i, j, ldelim, rdelim)
				continue
Rob Pike's avatar
Rob Pike committed
588
			}
589 590 591
			var b bytes.Buffer
			err = tmpl.Execute("hello", &b)
			s := b.String()
592
			if s != "template: hello"+ldelim+rdelim {
593
				t.Errorf("failed delim check(%q %q) %q got %q", ldelim, rdelim, text, s)
Rob Pike's avatar
Rob Pike committed
594 595 596 597
			}
		}
	}
}
Rob Pike's avatar
Rob Pike committed
598 599 600

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

605 606 607
	var buf bytes.Buffer
	input := "{.section @}{innerPointerT}{.end}"
	tmpl, err := Parse(input, nil)
Rob Pike's avatar
Rob Pike committed
608
	if err != nil {
609
		t.Fatal("unexpected parse error:", err)
Rob Pike's avatar
Rob Pike committed
610
	}
611
	err = tmpl.Execute(s, &buf)
Rob Pike's avatar
Rob Pike committed
612
	if err != nil {
613
		t.Fatal("unexpected execute error:", err)
Rob Pike's avatar
Rob Pike committed
614
	}
615
	expect := fmt.Sprintf("%v", &t1) // output should be hex address of t1
616
	if buf.String() != expect {
617
		t.Errorf("for %q: expected %q got %q", input, expect, buf.String())
Rob Pike's avatar
Rob Pike committed
618 619
	}
}
620 621 622 623 624

func TestHTMLFormatterWithByte(t *testing.T) {
	s := "Test string."
	b := []byte(s)
	var buf bytes.Buffer
625
	HTMLFormatter(&buf, "", b)
626 627 628 629 630
	bs := buf.String()
	if bs != s {
		t.Errorf("munged []byte, expected: %s got: %s", s, bs)
	}
}