tpls

Extendable, Fast Template Engine for Go
git clone git://git.lair.cx/tpls
Log | Files | Refs | README | LICENSE

commit 1848cd2f09d35f76e70a15b0dffbb84b615961a7
parent 8664ce4f9243f1278df70f02098658b8af015ba3
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Wed, 26 Jan 2022 04:08:41 +0900

Added tests for built-in tags

Diffstat:
Atplc/builtin_fallback_test.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_for_test.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_if_test.go | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_import_test.go | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_resources_test.go | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_template_test.go | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_test.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/builtin_typedef_test.go | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atplc/tplc_commons_test.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 708 insertions(+), 0 deletions(-)

diff --git a/tplc/builtin_fallback_test.go b/tplc/builtin_fallback_test.go @@ -0,0 +1,63 @@ +package tplc + +import ( + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_TagFallback(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + }{ + { + "self-closing tag", + "<test />", + false, + }, + { + "empty tag", + "<test></test>", + false, + }, + { + "with contents", + "<test><div>hi, this is dummy. <a>Aaa-</a></div></test>", + false, + }, + { + "with plain texts", + "<test>Hello, World!</test>", + false, + }, + { + "with incomplete content", + "<test><br></test>", + false, + }, + { + "incomplete", + "<test>dummy.", + true, + }, + } + tagHandler := TagFallback("test") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + if err := tagHandler(writer.builder, writer, tokenizer); (err != nil) != tt.wantErr { + t.Errorf("TagFallback.func() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/tplc/builtin_for_test.go b/tplc/builtin_for_test.go @@ -0,0 +1,72 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_TagFor(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + want string + }{ + { + "self-closing tag", + `<for />`, + false, + "for {\n}\n", + }, + { + "empty tag", + `<for></for>`, + false, + "for {\n}\n", + }, + { + "infinity loop", + `<for>Hello, World!</for>`, + false, + "for {\nw.WriteRaw(`Hello, World!`)\n}\n", + }, + { + "for range", + `<for class="a := range []int{1, 2, 3}"><integer>a</integer></for>`, + false, + "for a := range []int{1, 2, 3} {\nw.WriteInteger(a)\n}\n", + }, + { + "incomplete for", + `<for class="true">for`, + true, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + err := TagFor(writer.builder, writer, tokenizer) + if (err != nil) != tt.wantErr { + t.Errorf("TagIf() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("renderTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/builtin_if_test.go b/tplc/builtin_if_test.go @@ -0,0 +1,108 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_TagIf(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + want string + }{ + { + "self-closing tag", + `<if class="true" />`, + false, + "if true {\n}\n", + }, + { + "self-closing then", + `<if class="true"><then /></if>`, + false, + "if true {\n}\n", + }, + { + "self-closing else", + `<if class="true"><then>Hello, World!</then><else /></if>`, + false, + "if true {\nw.WriteRaw(`Hello, World!`)\n} else {\n}\n", + }, + { + "empty tag", + `<if class="true"></if>`, + false, + "if true {\n}\n", + }, + { + "if then", + `<if class="true"><then>Hello, World!</then></if>`, + false, + "if true {\nw.WriteRaw(`Hello, World!`)\n}\n", + }, + { + "if then else", + `<if class="true"><then>Hello, World!</then><else>true is !true</else></if>`, + false, + "if true {\nw.WriteRaw(`Hello, World!`)\n} else {\nw.WriteRaw(`true is !true`)\n}\n", + }, + { + "if then else-if else", + `<if class="true"><then>Hello, World!</then><else class="1 == 1">true is !true but 1 == 1</else><else>1 != 1</else></if>`, + false, + "if true {\nw.WriteRaw(`Hello, World!`)\n} else if 1 == 1 {\nw.WriteRaw(`true is !true but 1 == 1`)\n} else {\nw.WriteRaw(`1 != 1`)\n}\n", + }, + { + "incomplete if", + `<if class="true"><then>Hello, World!</then>`, + true, + "", + }, + { + "incomplete then", + `<if class="true"><then>Hello, World!</if>`, + true, + "", + }, + { + "incomplete else", + `<if class="true"><then>Hello, World!</then><else>Else</if>`, + true, + "", + }, + { + "plain text in if", + `<if class="true">Hello, World!</if>`, + true, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + err := TagIf(writer.builder, writer, tokenizer) + if (err != nil) != tt.wantErr { + t.Errorf("TagIf() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("renderTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/builtin_import_test.go b/tplc/builtin_import_test.go @@ -0,0 +1,66 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_TagImport(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + want string + }{ + { + "self-closing tag", + "<import />", + false, + "", + }, + { + "empty tag", + "<import></import>", + false, + "", + }, + { + "import pkg", + "<import>\"pkg\"</import>", + false, + "import (\"pkg\")\n\n", + }, + { + "incomplete", + "<import>\"pkg\"", + true, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + err := TagImport(writer.builder, writer, tokenizer) + if (err != nil) != tt.wantErr { + t.Errorf("TagImport() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("renderTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/builtin_resources_test.go b/tplc/builtin_resources_test.go @@ -0,0 +1,69 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_handleResourceTag(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + want string + }{ + { + "self-closing tag", + "<test />", + true, + "", + }, + { + "empty tag", + "<test></test>", + true, + "", + }, + { + "hello world", + "<test>Hello, World!</test>", + false, + "Hello, World!", + }, + { + "incomplete", + "<test>Hello, World!", + true, + "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + testResourceHandler := func(p []byte) { + writer.WriteGoCodeBytes(p) + } + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + err := handleResourceTag(tokenizer, "test", testResourceHandler) + if (err != nil) != tt.wantErr { + t.Errorf("handleResourceTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("renderTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/builtin_template_test.go b/tplc/builtin_template_test.go @@ -0,0 +1,100 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_renderTag(t *testing.T) { + tests := []struct { + name string + src string + tagName string + customHandlers map[string]TagHandler + useCustomOnly bool + wantErr bool + want string + }{ + { + "root document", + `<raw>fmt.Println("Hello, World!")</raw>`, + "", + nil, + false, + false, + "fmt.Println(\"Hello, World!\")\n", + }, + { + "with wrapper", + // When renderTags is called with tagName, starting tag is + // already parsed. + `<raw>fmt.Println("Hello, World!")</raw></wrapper>`, + "wrapper", + nil, + false, + false, + "fmt.Println(\"Hello, World!\")\n", + }, + { + "unknown tag with custom handler only flag", + `<div>Hello!</div></test>`, + "test", + map[string]TagHandler{}, + true, + true, + "", + }, + { + "custom tag with standard tag", + `<raw>fmt.Println("Hello, World!")</raw> + <custom>Hi!</custom></test>`, + "test", + map[string]TagHandler{ + "custom": TagTestTransparent("custom"), + }, + false, + false, + "fmt.Println(\"Hello, World!\")\nHi!", + }, + { + "with self-closing content", + `<custom /></test>`, + "test", + map[string]TagHandler{ + "custom": func(b *Builder, w *Writer, t *htmlx.Tokenizer) error { + if t.CurrentType() != htmlx.SelfClosingTagToken { + w.WriteGoCode("Not Good") + return TagTestTransparent("custom")(b, w, t) + } + w.WriteGoCode("Good") + return nil + }, + }, + true, + false, + "Good", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + err := renderTag(writer.builder, writer, tokenizer, tt.tagName, tt.customHandlers, tt.useCustomOnly) + if (err != nil) != tt.wantErr { + t.Errorf("renderTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("renderTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/builtin_test.go b/tplc/builtin_test.go @@ -0,0 +1,72 @@ +package tplc + +import ( + "bytes" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_getBodyFromTextOnlyTag(t *testing.T) { + tests := []struct { + name string + src string + allowEmpty bool + want []byte + wantErr bool + }{ + { + "with content", + `<test>ok</test>`, + false, + []byte("ok"), + false, + }, + { + "empty body but not allowed", + `<test></test>`, + false, + nil, + true, + }, + { + "allow empty", + `<test></test>`, + true, + nil, + false, + }, + { + "self-closing with allow empty", + `<test />`, + true, + nil, + false, + }, + { + "self-closing without allow empty", + `<test />`, + false, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + got, err := getBodyFromTextOnlyTag(tokenizer, "test", tt.allowEmpty) + if (err != nil) != tt.wantErr { + t.Errorf("getBodyFromTextOnlyTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Equal(got, tt.want) { + t.Errorf("getBodyFromTextOnlyTag() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/tplc/builtin_typedef_test.go b/tplc/builtin_typedef_test.go @@ -0,0 +1,66 @@ +package tplc + +import ( + "strconv" + "strings" + "testing" + + "go.lair.cx/go-core/net/htmlx" +) + +func Test_handleTypeTag(t *testing.T) { + tests := []struct { + name string + src string + wantErr bool + want string + }{ + { + "self-closing tag", + "<interface class=\"test\" />", + false, + "type test interface {}\n\n", + }, + { + "empty tag", + "<interface class=\"test\"></interface>", + false, + "type test interface {}\n\n", + }, + { + "without class name", + "<interface>A()</interface>", + true, + "", + }, + { + "with content", + "<interface class=\"test\">A()</interface>", + false, + "type test interface {A()}\n\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + writer = NewTestWriter() + tokenizer = htmlx.NewTokenizer(strings.NewReader(tt.src)) + ) + if kind := tokenizer.Next(); kind != htmlx.SelfClosingTagToken && kind != htmlx.StartTagToken { + t.Errorf("invalid test: unexpected token: %s", kind.String()) + return + } + err := handleTypeTag(writer, tokenizer, "interface") + if (err != nil) != tt.wantErr { + t.Errorf("handleTypeTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if got := writer.String(); got != tt.want { + t.Errorf("handleTypeTag() got = %s, want = %s", strconv.Quote(got), strconv.Quote(tt.want)) + } + }) + } +} diff --git a/tplc/tplc_commons_test.go b/tplc/tplc_commons_test.go @@ -0,0 +1,92 @@ +package tplc + +import ( + "bytes" + "fmt" + "io" + "strconv" + + "github.com/valyala/bytebufferpool" + + "go.lair.cx/go-core/net/htmlx" + "go.lair.cx/tpls/internal/stacks" +) + +func NewTestWriter() *Writer { + return &Writer{ + builder: New().UseBuiltin(), + } +} + +func NewTestBuilder(useBuiltin bool, tags map[string]TagHandler) *Builder { + b := New() + if useBuiltin { + b = b.UseBuiltin() + } + for key, tag := range tags { + b = b.AddTag(key, tag) + } + return b +} + +func TagTestTransparent(name string) TagHandler { + return func(b *Builder, w *Writer, t *htmlx.Tokenizer) error { + if t.CurrentType() == htmlx.SelfClosingTagToken { + return nil + } + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + stackBuf := bytebufferpool.Get() + defer bytebufferpool.Put(stackBuf) + + stack := stacks.NewByteStack(stackBuf.B) + + loop: + for { + switch t.Next() { + case htmlx.StartTagToken: + tagName, _ := t.TagName() + stack.Put(tagName) + + case htmlx.EndTagToken: + tagName, _ := t.TagName() + + // Remove items in stack until the matched starting tag is found. + // If the tag is not exists in stack, + // - Root tag's closing. + // - Unexpected closing tag. + for { + item, ok := stack.Pop() + if !ok { // Tag is not found: + if string(tagName) == name { + break loop + } + return fmt.Errorf( + "unexpected closing tag: %s", + strconv.Quote(string(tagName)), + ) + } + if bytes.Equal(item, tagName) { + break + } + } + + case htmlx.ErrorToken: + err := t.Err() + if err == io.EOF { + if len(name) == 0 && stack.Len() == 0 { + return nil + } + return io.ErrUnexpectedEOF + } + return err + } + + w.WriteGoCodeBytes(t.Raw()) + } + + return nil + } +}