From caca7a2b60e4ae548435bcd4254a5fadbffbf416 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Sun, 2 Jul 2017 07:51:24 -0400 Subject: [PATCH] Coverage --- .gitignore | 1 + chunks/control_tags.go | 6 +-- chunks/marshal.go | 2 +- chunks/parser_test.go | 14 +++++-- chunks/render_test.go | 44 ++++++++++++++++++++-- chunks/scanner_test.go | 6 ++- expressions/expressions_test.go | 13 +++++-- expressions/parser.go | 2 +- expressions/parser_test.go | 4 +- expressions/scanner.go | 67 ++++++++++++++++----------------- expressions/scanner.rl | 4 +- expressions/scanner_test.go | 6 +++ generics/convert.go | 5 ++- generics/generics_test.go | 18 ++++++++- liquid_test.go | 63 ++++++++++++++++++++++++++++++- scripts/coverage | 12 ++++++ 16 files changed, 209 insertions(+), 58 deletions(-) create mode 100755 scripts/coverage diff --git a/.gitignore b/.gitignore index 78f4fea..dacfbd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.output liquid +*.out diff --git a/chunks/control_tags.go b/chunks/control_tags.go index 9457421..2fbf6df 100644 --- a/chunks/control_tags.go +++ b/chunks/control_tags.go @@ -71,11 +71,11 @@ func (b tagBuilder) Governs(_ []string) tagBuilder { // SameSyntaxAs tells the parser that this tag has the same syntax as the named tag. func (b tagBuilder) SameSyntaxAs(name string) tagBuilder { - ot := b.s.controlTags[name] - if ot == nil { + rt := b.s.controlTags[name] + if rt == nil { panic(fmt.Errorf("undefined: %s", name)) } - b.tag.syntaxModel = ot + b.tag.syntaxModel = rt return b } diff --git a/chunks/marshal.go b/chunks/marshal.go index f08cd67..1fd572c 100644 --- a/chunks/marshal.go +++ b/chunks/marshal.go @@ -23,7 +23,7 @@ func (c Chunk) MarshalYAML() (interface{}, error) { case TagChunkType: return map[string]interface{}{"tag": c.Name, "args": c.Args}, nil case ObjChunkType: - return map[string]interface{}{"obj": c.Name}, nil + return map[string]interface{}{"obj": c.Args}, nil default: return nil, fmt.Errorf("unknown chunk tag type: %v", c.Type) } diff --git a/chunks/parser_test.go b/chunks/parser_test.go index 928bd4c..0a3bb55 100644 --- a/chunks/parser_test.go +++ b/chunks/parser_test.go @@ -2,22 +2,29 @@ package chunks import ( "fmt" + "io" "testing" "github.com/stretchr/testify/require" ) -func addTestTags(s Settings) { +func addParserTestTags(s Settings) { s.AddStartTag("case").Branch("when") s.AddStartTag("comment") s.AddStartTag("for").Governs([]string{"break"}) s.AddStartTag("if").Branch("else").Branch("elsif") + s.AddStartTag("unless").SameSyntaxAs("if") s.AddStartTag("raw") + s.AddStartTag("err1").Parser(func(c ASTControlTag) (func(io.Writer, RenderContext) error, error) { + return nil, fmt.Errorf("stage 1 error") + }) } var parseErrorTests = []struct{ in, expected string }{ {"{%unknown_tag%}", "unknown tag"}, {"{%if test%}", "unterminated if tag"}, + {"{%if test%}{% endunless %}", "not inside unless"}, + {`{% err1 %}{% enderr1 %}`, "stage 1 error"}, // {"{%for syntax error%}{%endfor%}", "parse error"}, } @@ -25,13 +32,14 @@ var parserTests = []struct{ in string }{ {`{% for item in list %}{% endfor %}`}, {`{% if test %}{% else %}{% endif %}`}, {`{% if test %}{% if test %}{% endif %}{% endif %}`}, + {`{% unless test %}{% else %}{% endunless %}`}, {`{% for item in list %}{% if test %}{% else %}{% endif x %}{% endfor %}`}, {`{% if true %}{% raw %}{% endraw %}{% endif %}`}, } func TestParseErrors(t *testing.T) { settings := NewSettings() - addTestTags(settings) + addParserTestTags(settings) for i, test := range parseErrorTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { ast, err := settings.Parse(test.in) @@ -44,7 +52,7 @@ func TestParseErrors(t *testing.T) { func TestParser(t *testing.T) { settings := NewSettings() - addTestTags(settings) + addParserTestTags(settings) for i, test := range parserTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { _, err := settings.Parse(test.in) diff --git a/chunks/render_test.go b/chunks/render_test.go index 1363924..4114d6f 100644 --- a/chunks/render_test.go +++ b/chunks/render_test.go @@ -3,17 +3,39 @@ package chunks import ( "bytes" "fmt" + "io" + "io/ioutil" "testing" "github.com/stretchr/testify/require" ) -var renderTests = []struct{ in, expected string }{ - // {"{%if syntax error%}{%endif%}", "parse error"}, +func addRenderTestTags(s Settings) { + s.AddStartTag("parse").Parser(func(c ASTControlTag) (func(io.Writer, RenderContext) error, error) { + a := c.Args + return func(w io.Writer, c RenderContext) error { + _, err := w.Write([]byte(a)) + return err + }, nil + }) + s.AddStartTag("err2").Parser(func(c ASTControlTag) (func(io.Writer, RenderContext) error, error) { + return func(w io.Writer, c RenderContext) error { + return fmt.Errorf("stage 2 error") + }, nil + }) +} + +var renderTests = []struct{ in, out string }{ {`{{ 12 }}`, "12"}, {`{{ x }}`, "123"}, {`{{ page.title }}`, "Introduction"}, {`{{ ar[1] }}`, "second"}, + {`{% parse args %}{% endparse %}`, "args"}, +} + +var renderErrorTests = []struct{ in, out string }{ + // {"{%if syntax error%}{%endif%}", "parse error"}, + {`{% err2 %}{% enderr2 %}`, "stage 2 error"}, } var renderTestBindings = map[string]interface{}{ @@ -45,6 +67,7 @@ var renderTestBindings = map[string]interface{}{ func TestRender(t *testing.T) { settings := NewSettings() + addRenderTestTags(settings) context := NewContext(renderTestBindings, settings) for i, test := range renderTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { @@ -53,7 +76,22 @@ func TestRender(t *testing.T) { buf := new(bytes.Buffer) err = ast.Render(buf, context) require.NoErrorf(t, err, test.in) - require.Equalf(t, test.expected, buf.String(), test.in) + require.Equalf(t, test.out, buf.String(), test.in) + }) + } +} + +func TestRenderErrors(t *testing.T) { + settings := NewSettings() + addRenderTestTags(settings) + context := NewContext(renderTestBindings, settings) + for i, test := range renderErrorTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + ast, err := settings.Parse(test.in) + require.NoErrorf(t, err, test.in) + err = ast.Render(ioutil.Discard, context) + require.Errorf(t, err, test.in) + require.Containsf(t, err.Error(), test.out, test.in) }) } } diff --git a/chunks/scanner_test.go b/chunks/scanner_test.go index cb2dfb9..0554a5c 100644 --- a/chunks/scanner_test.go +++ b/chunks/scanner_test.go @@ -22,7 +22,7 @@ var scannerCountTests = []struct { {`{{ expr arg }}{{ expr arg }}`, 2}, } -func TestScanner(t *testing.T) { +func TestChunkScanner(t *testing.T) { tokens := Scan("12", "") require.NotNil(t, tokens) require.Len(t, tokens, 1) @@ -55,6 +55,10 @@ func TestScanner(t *testing.T) { require.Equal(t, "tag", tokens[0].Name) require.Equal(t, "args", tokens[0].Args) + tokens = Scan("pre{% tag args %}mid{{ object }}post", "") + require.Equal(t, `[TextChunkType{"pre"} TagChunkType{Tag:"tag", Args:"args"} TextChunkType{"mid"} ObjChunkType{"object"} TextChunkType{"post"}]`, fmt.Sprint(tokens)) + require.Equal(t, "- text: pre\n- args: args\n tag: tag\n- text: mid\n- obj: object\n- text: post\n", MustYAML(tokens)) + for i, test := range scannerCountTests { t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { tokens := Scan(test.in, "") diff --git a/expressions/expressions_test.go b/expressions/expressions_test.go index cdc26aa..46940d6 100644 --- a/expressions/expressions_test.go +++ b/expressions/expressions_test.go @@ -2,6 +2,7 @@ package expressions import ( "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -91,9 +92,12 @@ var evaluatorTests = []struct { {`"seafood" contains "foo"`, true}, {`"seafood" contains "bar"`, false}, + + // filters + {`"seafood" | length`, 8}, } -var evaluatorTestContext = NewContext(map[string]interface{}{ +var evaluatorTestBindings = (map[string]interface{}{ "n": 123, "array": []string{"first", "second", "third"}, "empty_list": []interface{}{}, @@ -103,12 +107,15 @@ var evaluatorTestContext = NewContext(map[string]interface{}{ "b": map[string]interface{}{"c": "d"}, "c": []string{"r", "g", "b"}, }, -}, NewSettings()) +}) func TestEvaluator(t *testing.T) { + settings := NewSettings() + settings.AddFilter("length", strings.Count) + context := NewContext(evaluatorTestBindings, settings) for i, test := range evaluatorTests { t.Run(fmt.Sprint(i), func(t *testing.T) { - val, err := EvaluateString(test.in, evaluatorTestContext) + val, err := EvaluateString(test.in, context) require.NoErrorf(t, err, test.in) require.Equalf(t, test.expected, val, test.in) }) diff --git a/expressions/parser.go b/expressions/parser.go index 711eae9..62dc421 100644 --- a/expressions/parser.go +++ b/expressions/parser.go @@ -39,7 +39,7 @@ func Parse(source string) (expr Expression, err error) { lexer := newLexer([]byte(source + ";")) n := yyParse(lexer) if n != 0 { - return nil, fmt.Errorf("parse error in %s", source) + return nil, fmt.Errorf("parse error in %q", source) } return &expression{lexer.val}, nil } diff --git a/expressions/parser_test.go b/expressions/parser_test.go index 5b8b0ba..58643f7 100644 --- a/expressions/parser_test.go +++ b/expressions/parser_test.go @@ -8,10 +8,10 @@ import ( ) var parseErrorTests = []struct{ in, expected string }{ -// {"a | unknown_filter", "undefined filter: unknown_filter"}, + {"a syntax error", "parse error"}, } -func TestParseErrors(t *testing.T) { +func TestExpressionParseErrors(t *testing.T) { for i, test := range parseErrorTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { expr, err := Parse(test.in) diff --git a/expressions/scanner.go b/expressions/scanner.go index 7ffddff..9270cc6 100644 --- a/expressions/scanner.go +++ b/expressions/scanner.go @@ -1,13 +1,12 @@ //line scanner.rl:1 package expressions -// Adapted from https://github.com/mhamrah/thermostat import "fmt" import "strconv" -//line scanner.go:11 +//line scanner.go:10 var _expression_actions []byte = []byte{ 0, 1, 0, 1, 1, 1, 2, 1, 12, 1, 13, 1, 14, 1, 15, 1, 16, @@ -176,7 +175,7 @@ const expression_error int = -1 const expression_en_main int = 11 -//line scanner.rl:13 +//line scanner.rl:12 type lexer struct { @@ -196,7 +195,7 @@ func newLexer(data []byte) *lexer { pe: len(data), } -//line scanner.go:200 +//line scanner.go:199 { lex.cs = expression_start lex.ts = 0 @@ -204,7 +203,7 @@ func newLexer(data []byte) *lexer { lex.act = 0 } -//line scanner.rl:32 +//line scanner.rl:31 return lex } @@ -213,7 +212,7 @@ func (lex *lexer) Lex(out *yySymType) int { tok := 0 -//line scanner.go:217 +//line scanner.go:216 { var _klen int var _trans int @@ -233,7 +232,7 @@ _resume: //line NONE:1 lex.ts = ( lex.p) -//line scanner.go:237 +//line scanner.go:236 } } @@ -308,44 +307,44 @@ _eof_trans: lex.te = ( lex.p)+1 case 3: -//line scanner.rl:59 +//line scanner.rl:58 lex.act = 4; case 4: -//line scanner.rl:40 +//line scanner.rl:39 lex.act = 6; case 5: -//line scanner.rl:95 +//line scanner.rl:94 lex.act = 11; case 6: -//line scanner.rl:96 +//line scanner.rl:95 lex.act = 12; case 7: -//line scanner.rl:97 +//line scanner.rl:96 lex.act = 13; case 8: -//line scanner.rl:98 +//line scanner.rl:97 lex.act = 14; case 9: -//line scanner.rl:99 +//line scanner.rl:98 lex.act = 15; case 10: -//line scanner.rl:45 +//line scanner.rl:44 lex.act = 17; case 11: -//line scanner.rl:103 +//line scanner.rl:102 lex.act = 19; case 12: -//line scanner.rl:85 +//line scanner.rl:84 lex.te = ( lex.p)+1 { tok = ASSIGN; ( lex.p)++; goto _out } case 13: -//line scanner.rl:86 +//line scanner.rl:85 lex.te = ( lex.p)+1 { tok = LOOP; ( lex.p)++; goto _out } case 14: -//line scanner.rl:68 +//line scanner.rl:67 lex.te = ( lex.p)+1 { tok = LITERAL @@ -355,37 +354,37 @@ _eof_trans: } case 15: -//line scanner.rl:91 +//line scanner.rl:90 lex.te = ( lex.p)+1 { tok = EQ; ( lex.p)++; goto _out } case 16: -//line scanner.rl:92 +//line scanner.rl:91 lex.te = ( lex.p)+1 { tok = NEQ; ( lex.p)++; goto _out } case 17: -//line scanner.rl:93 +//line scanner.rl:92 lex.te = ( lex.p)+1 { tok = GE; ( lex.p)++; goto _out } case 18: -//line scanner.rl:94 +//line scanner.rl:93 lex.te = ( lex.p)+1 { tok = LE; ( lex.p)++; goto _out } case 19: -//line scanner.rl:100 +//line scanner.rl:99 lex.te = ( lex.p)+1 { tok = KEYWORD; out.name = string(lex.data[lex.ts:lex.te-1]); ( lex.p)++; goto _out } case 20: -//line scanner.rl:103 +//line scanner.rl:102 lex.te = ( lex.p)+1 { tok = int(lex.data[lex.ts]); ( lex.p)++; goto _out } case 21: -//line scanner.rl:50 +//line scanner.rl:49 lex.te = ( lex.p) ( lex.p)-- { @@ -399,7 +398,7 @@ _eof_trans: } case 22: -//line scanner.rl:45 +//line scanner.rl:44 lex.te = ( lex.p) ( lex.p)-- { @@ -409,18 +408,18 @@ _eof_trans: } case 23: -//line scanner.rl:102 +//line scanner.rl:101 lex.te = ( lex.p) ( lex.p)-- case 24: -//line scanner.rl:103 +//line scanner.rl:102 lex.te = ( lex.p) ( lex.p)-- { tok = int(lex.data[lex.ts]); ( lex.p)++; goto _out } case 25: -//line scanner.rl:103 +//line scanner.rl:102 ( lex.p) = ( lex.te) - 1 { tok = int(lex.data[lex.ts]); ( lex.p)++; goto _out } @@ -481,7 +480,7 @@ _eof_trans: } } -//line scanner.go:485 +//line scanner.go:484 } } @@ -495,7 +494,7 @@ _again: //line NONE:1 lex.ts = 0 -//line scanner.go:499 +//line scanner.go:498 } } @@ -514,12 +513,12 @@ _again: _out: {} } -//line scanner.rl:107 +//line scanner.rl:106 return tok } func (lex *lexer) Error(e string) { - fmt.Println("error:", e) + // fmt.Println("scan error:", e) } \ No newline at end of file diff --git a/expressions/scanner.rl b/expressions/scanner.rl index 27c23ab..f06d4fb 100644 --- a/expressions/scanner.rl +++ b/expressions/scanner.rl @@ -109,5 +109,5 @@ func (lex *lexer) Lex(out *yySymType) int { } func (lex *lexer) Error(e string) { - fmt.Println("error:", e) -} \ No newline at end of file + // fmt.Println("scan error:", e) +} diff --git a/expressions/scanner_test.go b/expressions/scanner_test.go index 63f8745..779a40a 100644 --- a/expressions/scanner_test.go +++ b/expressions/scanner_test.go @@ -32,4 +32,10 @@ func TestExpressionScanner(t *testing.T) { tokens, err := scanExpression("abc > 123") require.NoError(t, err) require.Len(t, tokens, 3) + + tokens, _ = scanExpression("forage") + require.Len(t, tokens, 1) + + tokens, _ = scanExpression("orange") + require.Len(t, tokens, 1) } diff --git a/generics/convert.go b/generics/convert.go index 8ee3546..f16e7f8 100644 --- a/generics/convert.go +++ b/generics/convert.go @@ -25,7 +25,8 @@ func conversionError(modifier string, value interface{}, typ reflect.Type) error // handle circular references. func Convert(value interface{}, target reflect.Type) (interface{}, error) { // nolint: gocyclo r := reflect.ValueOf(value) - if r.Type().ConvertibleTo(target) { + // convert int.Convert(string) yields "\x01" not "1" + if target.Kind() != reflect.String && r.Type().ConvertibleTo(target) { return r.Convert(target).Interface(), nil } if reflect.PtrTo(r.Type()) == target { @@ -97,6 +98,8 @@ func Convert(value interface{}, target reflect.Type) (interface{}, error) { // n } return out.Interface(), nil } + case reflect.String: + return fmt.Sprint(value), nil } return nil, conversionError("", value, target) } diff --git a/generics/generics_test.go b/generics/generics_test.go index d9f3286..fa1765e 100644 --- a/generics/generics_test.go +++ b/generics/generics_test.go @@ -16,6 +16,10 @@ var convertTests = []struct { {"1.2", 1.0, float64(1.2)}, {true, 1, 1}, {false, 1, 0}, + {1, "", "1"}, + {false, "", "false"}, + {true, "", "true"}, + {"string", "", "string"}, } var eqTests = []struct { @@ -63,13 +67,23 @@ var lessTests = []struct { {[]string{"a"}, []string{"a"}, false}, } +func TestCall(t *testing.T) { + fn := func(a, b string) string { + return a + "," + b + "." + } + args := []interface{}{5, 10} + value, err := Call(reflect.ValueOf(fn), args) + require.NoError(t, err) + require.Equal(t, "5,10.", value) +} func TestConvert(t *testing.T) { for i, test := range convertTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { typ := reflect.TypeOf(test.proto) value, err := Convert(test.value, typ) - require.NoError(t, err) - require.Equalf(t, test.expected, value, "Convert %#v -> %#v", test.value, test, typ) + name := fmt.Sprintf("Convert %#v -> %v", test.value, typ) + require.NoErrorf(t, err, name) + require.Equalf(t, test.expected, value, name) }) } } diff --git a/liquid_test.go b/liquid_test.go index ae099cf..f1814bf 100644 --- a/liquid_test.go +++ b/liquid_test.go @@ -2,9 +2,12 @@ package liquid import ( "fmt" + "io" "log" + "strings" "testing" + "github.com/osteele/liquid/chunks" "github.com/stretchr/testify/require" ) @@ -37,9 +40,9 @@ func TestLiquid(t *testing.T) { func Example() { engine := NewEngine() - template := `

{{page.title}}

` + template := `

{{ page.title }}

` bindings := map[string]interface{}{ - "page": map[string]interface{}{ + "page": map[string]string{ "title": "Introduction", }, } @@ -51,3 +54,59 @@ func Example() { fmt.Println(out) // Output:

Introduction

} + +func Example_filter() { + engine := NewEngine() + engine.DefineFilter("has_prefix", strings.HasPrefix) + template := `{{ title | has_prefix: "Intro" }}` + + bindings := map[string]interface{}{ + "title": "Introduction", + } + out, err := engine.ParseAndRenderString(template, NewContext(bindings)) + if err != nil { + log.Fatalln(err) + } + fmt.Println(out) + // Output: true +} + +func Example_tag() { + engine := NewEngine() + engine.DefineTag("echo", func(w io.Writer, c chunks.RenderContext) error { + args := c.TagArgs() + _, err := w.Write([]byte(args)) + return err + }) + + template := `{% echo hello world %}` + bindings := map[string]interface{}{} + out, err := engine.ParseAndRenderString(template, NewContext(bindings)) + if err != nil { + log.Fatalln(err) + } + fmt.Println(out) + // Output: hello world +} + +func Example_tag_pair() { + engine := NewEngine() + engine.DefineStartTag("length", func(w io.Writer, c chunks.RenderContext) error { + s, err := c.InnerString() + if err != nil { + return err + } + n := len(s) + _, err = w.Write([]byte(fmt.Sprint(n))) + return err + }) + + template := `{% length %}abc{% endlength %}` + bindings := map[string]interface{}{} + out, err := engine.ParseAndRenderString(template, NewContext(bindings)) + if err != nil { + log.Fatalln(err) + } + fmt.Println(out) + // Output: 3 +} diff --git a/scripts/coverage b/scripts/coverage new file mode 100755 index 0000000..d67a4ae --- /dev/null +++ b/scripts/coverage @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +echo 'mode: set' > coverage.out + +for p in $(go list -f '{{.ImportPath}}' ./...); do + rm -f package-coverage.out + go test -coverprofile=package-coverage.out $p + [[ -f package-coverage.out ]] && grep -v 'mode: set' package-coverage.out >> coverage.out + rm -f package-coverage.out +done