From 29c902f6b33db85a1caff3e93de318253e7658d2 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Sat, 15 Jul 2017 12:13:07 -0400 Subject: [PATCH] Coverage --- README.md | 2 +- cmd/liquid/main.go | 36 +++++++++++++++++------ cmd/liquid/main_test.go | 17 +++++++++++ evaluator/compare_test.go | 49 ++++++++++++++++++++++++++++++++ evaluator/convert_test.go | 9 ++++++ evaluator/evaluator_test.go | 34 ---------------------- expressions/filters.go | 2 +- expressions/filters_test.go | 10 +++++++ expressions/parser_test.go | 38 +++++++++++++++++++++++-- filters/standard_filters_test.go | 9 ++++++ render/render_test.go | 1 + 11 files changed, 159 insertions(+), 48 deletions(-) create mode 100644 cmd/liquid/main_test.go create mode 100644 evaluator/compare_test.go diff --git a/README.md b/README.md index d717f05..cd6325f 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ Please refer to the [contribution guidelines](./CONTRIBUTING.md). | Package | Author | Description | License | |-----------------------------------------------------|-----------------|-----------------------------------------|--------------------| -| [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) | Canonical | YAML support (for printing parse trees) | Apache License 2.0 | | [Ragel](http://www.colm.net/open-source/ragel/) | Adrian Thurston | scanning expressions | MIT | +| [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) | Canonical | YAML support (for printing parse trees) | Apache License 2.0 | Michael Hamrah's [Lexing with Ragel and Parsing with Yacc using Go](https://medium.com/@mhamrah/lexing-with-ragel-and-parsing-with-yacc-using-go-81e50475f88f) was essential to understanding `go yacc`. diff --git a/cmd/liquid/main.go b/cmd/liquid/main.go index 5e94f2f..cdf1867 100644 --- a/cmd/liquid/main.go +++ b/cmd/liquid/main.go @@ -19,32 +19,50 @@ import ( "github.com/osteele/liquid" ) +// for testing +var ( + stderr = os.Stderr + stdout io.Writer = os.Stdout + stdin io.Reader = os.Stdin + exit = os.Exit +) + func main() { - args := os.Args[1:] + if err := run(os.Args[1:]); err != nil { + fmt.Fprint(stderr, err) // nolint: gas + exit(1) + } +} + +func run(args []string) error { switch { case len(args) == 0: buf := new(bytes.Buffer) - _, err := io.Copy(buf, os.Stdin) - exitIfErr(err) + if _, err := io.Copy(buf, stdin); err != nil { + return err + } render(buf.Bytes(), "") case args[0] == "-h" || args[0] == "--help": usage(false) case strings.HasPrefix(args[0], "-"): usage(true) - os.Exit(1) + exit(1) case len(args) == 1: s, err := ioutil.ReadFile(args[0]) - exitIfErr(err) + if err != nil { + return err + } render(s, args[0]) default: usage(true) } + return nil } func exitIfErr(err error) { if err != nil { - fmt.Fprint(os.Stdout, err) // nolint: gas - os.Exit(1) + fmt.Fprint(stdout, err) // nolint: gas + exit(1) } } @@ -53,12 +71,12 @@ func render(b []byte, filename string) { exitIfErr(err) out, err := tpl.Render(map[string]interface{}{}) exitIfErr(err) - os.Stdout.Write(out) // nolint: gas, errcheck + stdout.Write(out) // nolint: gas, errcheck } func usage(error bool) { fmt.Printf("usage: %s [FILE]\n", os.Args[0]) if error { - os.Exit(1) + exit(1) } } diff --git a/cmd/liquid/main_test.go b/cmd/liquid/main_test.go new file mode 100644 index 0000000..e26cd83 --- /dev/null +++ b/cmd/liquid/main_test.go @@ -0,0 +1,17 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMain(t *testing.T) { + src := `{{ "Hello World" | downcase | split: " " | first | append: "!"}}` + stdin = bytes.NewBufferString(src) + buf := new(bytes.Buffer) + stdout = buf + require.NoError(t, run([]string{})) + require.Equal(t, "hello!", buf.String()) +} diff --git a/evaluator/compare_test.go b/evaluator/compare_test.go new file mode 100644 index 0000000..9cffbc1 --- /dev/null +++ b/evaluator/compare_test.go @@ -0,0 +1,49 @@ +package evaluator + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +var eqTestObj = struct{ a, b int }{1, 2} +var eqArrayTestObj = [2]int{1, 2} + +var eqTests = []struct { + a, b interface{} + expected bool +}{ + {nil, nil, true}, + {nil, 1, false}, + {1, nil, false}, + {false, false, true}, + {false, true, false}, + {0, 1, false}, + {1, 1, true}, + {1.0, 1.0, true}, + {1, 1.0, true}, + {1, 2.0, false}, + {1.0, 1, true}, + {"a", "b", false}, + {"a", "a", true}, + {int8(2), int16(2), true}, // TODO + // {uint8(2), int8(2), true}, // FIXME + {eqArrayTestObj, eqArrayTestObj[:], true}, + {[]string{"a"}, []string{"a"}, true}, + {[]string{"a"}, []string{"a", "b"}, false}, + {[]string{"a", "b"}, []string{"a"}, false}, + {[]string{"a", "b"}, []string{"a", "b"}, true}, + {[]string{"a", "b"}, []string{"a", "c"}, false}, + {[]interface{}{1.0, 2}, []interface{}{1, 2.0}, true}, + {eqTestObj, eqTestObj, true}, +} + +func TestEqual(t *testing.T) { + for i, test := range eqTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + value := Equal(test.a, test.b) + require.Equalf(t, test.expected, value, "%#v == %#v", test.a, test.b) + }) + } +} diff --git a/evaluator/convert_test.go b/evaluator/convert_test.go index 45d53de..87549ab 100644 --- a/evaluator/convert_test.go +++ b/evaluator/convert_test.go @@ -114,3 +114,12 @@ func TestConvert_map_to_array(t *testing.T) { // require.NotNil(t, ptr) // require.Equal(t, "ab", *ptr) // } + +func TestMustConvert(t *testing.T) { + typ := reflect.TypeOf("") + v := MustConvert(2, typ) + require.Equal(t, "2", v) + + typ = reflect.TypeOf(2) + require.Panics(t, func() { MustConvert("xxx", typ) }) +} diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index a4b13c2..0a6cbb8 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -7,31 +7,6 @@ import ( "github.com/stretchr/testify/require" ) -var eqTests = []struct { - a, b interface{} - expected bool -}{ - {nil, nil, true}, - {nil, 1, false}, - {1, nil, false}, - {false, false, true}, - {false, true, false}, - {0, 1, false}, - {1, 1, true}, - {1.0, 1.0, true}, - {1, 1.0, true}, - {1, 2.0, false}, - {1.0, 1, true}, - {"a", "b", false}, - {"a", "a", true}, - {[]string{"a"}, []string{"a"}, true}, - {[]string{"a"}, []string{"a", "b"}, false}, - {[]string{"a", "b"}, []string{"a"}, false}, - {[]string{"a", "b"}, []string{"a", "b"}, true}, - {[]string{"a", "b"}, []string{"a", "c"}, false}, - {[]interface{}{1.0, 2}, []interface{}{1, 2.0}, true}, -} - var lessTests = []struct { a, b interface{} expected bool @@ -57,15 +32,6 @@ func TestContains(t *testing.T) { require.False(t, Contains([]int{1, 2}, 3)) } -func TestEqual(t *testing.T) { - for i, test := range eqTests { - t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { - value := Equal(test.a, test.b) - require.Equalf(t, test.expected, value, "%#v == %#v", test.a, test.b) - }) - } -} - func TestLess(t *testing.T) { for i, test := range lessTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { diff --git a/expressions/filters.go b/expressions/filters.go index 55f6338..6b6d4ca 100644 --- a/expressions/filters.go +++ b/expressions/filters.go @@ -31,7 +31,7 @@ func (c *Config) AddFilter(name string, fn interface{}) { panic(fmt.Errorf("a filter must be a function")) case rf.Type().NumIn() < 1: panic(fmt.Errorf("a filter function must have at least one input")) - case rf.Type().NumOut() > 2: + case rf.Type().NumOut() < 1 || 2 < rf.Type().NumOut(): panic(fmt.Errorf("a filter must be have one or two outputs")) // case rf.Type().Out(1).Implements(…): // panic(typeError("a filter's second output must be type error")) diff --git a/expressions/filters_test.go b/expressions/filters_test.go index 39148ef..eeff563 100644 --- a/expressions/filters_test.go +++ b/expressions/filters_test.go @@ -7,6 +7,16 @@ import ( "github.com/stretchr/testify/require" ) +func TestContext_AddFilter(t *testing.T) { + cfg := NewConfig() + require.NotPanics(t, func() { cfg.AddFilter("f", func(int) int { return 0 }) }) + require.NotPanics(t, func() { cfg.AddFilter("f", func(int) (a int, e error) { return }) }) + require.Panics(t, func() { cfg.AddFilter("f", func() int { return 0 }) }) + require.Panics(t, func() { cfg.AddFilter("f", func(int) {}) }) + // require.Panics(t, func() { cfg.AddFilter("f", func(int) (a int, b int) { return }) }) + require.Panics(t, func() { cfg.AddFilter("f", func(int) (a int, e error, b int) { return }) }) +} + func TestContext_runFilter(t *testing.T) { cfg := NewConfig() constant := func(value interface{}) valueFn { diff --git a/expressions/parser_test.go b/expressions/parser_test.go index 998da19..ecc86d1 100644 --- a/expressions/parser_test.go +++ b/expressions/parser_test.go @@ -12,13 +12,15 @@ var parseTests = []struct { expect interface{} }{ {`a | filter: b`, 3}, - // {`%assign a = 3`, nil}, - // {`{%cycle 'a'`, []interface{}{"a"}}, - // {`{%cycle 'a', 'b'`, []interface{}{"a", "b"}}, } var parseErrorTests = []struct{ in, expected string }{ {"a syntax error", "parse error"}, + {`%assign a`, "parse error"}, + {`%assign a 3`, "parse error"}, + {`%cycle 'a' 'b'`, "parse error"}, + {`%loop a in in`, "parse error"}, + {`%when a b`, "parse error"}, } func TestParse(t *testing.T) { @@ -37,6 +39,36 @@ func TestParse(t *testing.T) { } } +func TestParseStatement(t *testing.T) { + stmt, err := ParseStatement(AssignStatementSelector, "a = b") + require.NoError(t, err) + require.Equal(t, "a", stmt.Assignment.Variable) + + stmt, err = ParseStatement(CycleStatementSelector, "'a', 'b'") + require.NoError(t, err) + require.Equal(t, "", stmt.Cycle.Group) + require.Len(t, stmt.Cycle.Values, 2) + require.Equal(t, []string{"a", "b"}, stmt.Cycle.Values) + + stmt, err = ParseStatement(CycleStatementSelector, "'g': 'a', 'b'") + require.NoError(t, err) + require.Equal(t, "g", stmt.Cycle.Group) + require.Len(t, stmt.Cycle.Values, 2) + require.Equal(t, []string{"a", "b"}, stmt.Cycle.Values) + + stmt, err = ParseStatement(LoopStatementSelector, "x in array reversed offset: 2 limit: 3") + require.NoError(t, err) + require.Equal(t, "x", stmt.Loop.Variable) + require.True(t, stmt.Loop.Reversed) + require.Equal(t, 2, stmt.Loop.Offset) + require.NotNil(t, stmt.Loop.Limit) + require.Equal(t, 3, *stmt.Loop.Limit) + + stmt, err = ParseStatement(WhenStatementSelector, "a, b") + require.NoError(t, err) + require.Len(t, stmt.When.Exprs, 2) +} + func TestParse_errors(t *testing.T) { for i, test := range parseErrorTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { diff --git a/filters/standard_filters_test.go b/filters/standard_filters_test.go index 0def9c9..854ee04 100644 --- a/filters/standard_filters_test.go +++ b/filters/standard_filters_test.go @@ -25,6 +25,8 @@ var filterTests = []struct { {`pages | map: 'category' | join`, "business, celebrities, , lifestyle, sports, , technology"}, {`pages | map: 'category' | compact | join`, "business, celebrities, lifestyle, sports, technology"}, {`"John, Paul, George, Ringo" | split: ", " | join: " and "`, "John and Paul and George and Ringo"}, + // {`",John, Paul, George, Ringo" | split: ", " | join: " and "`, "John and Paul and George and Ringo"}, + // {`"John, Paul, George, Ringo," | split: ", " | join: " and "`, "John and Paul and George and Ringo"}, {`animals | sort | join: ", "`, "Sally Snake, giraffe, octopus, zebra"}, {`sort_prop | sort: "weight" | inspect`, `[{"weight":null},{"weight":1},{"weight":3},{"weight":5}]`}, {`fruits | reverse | join: ", "`, "plums, peaches, oranges, apples"}, @@ -67,6 +69,7 @@ var filterTests = []struct { {`"website.com" | append: "/index.html"`, "website.com/index.html"}, {`"title" | capitalize`, "Title"}, {`"my great title" | capitalize`, "My great title"}, + {`"" | capitalize`, ""}, {`"Parker Moore" | downcase`, "parker moore"}, {`"Have you read 'James & the Giant Peach'?" | escape`, "Have you read 'James & the Giant Peach'?"}, {`"1 < 2 & 3" | escape_once`, "1 < 2 & 3"}, @@ -79,7 +82,10 @@ var filterTests = []struct { {`"Liquid" | slice: 2`, "q"}, {`"Liquid" | slice: 2, 5`, "quid"}, {`"Liquid" | slice: -3, 2`, "ui"}, + {`"Have you read Ulysses?" | strip_html`, "Have you read Ulysses?"}, + {`string_with_newlines | strip_newlines`, "Hellothere"}, + {`"Ground control to Major Tom." | truncate: 20`, "Ground control to..."}, {`"Ground control to Major Tom." | truncate: 25, ", and so on"`, "Ground control, and so on"}, {`"Ground control to Major Tom." | truncate: 20, ""`, "Ground control to Ma"}, @@ -135,6 +141,7 @@ var filterTests = []struct { {`5 | divided_by: 3`, 1}, {`20 | divided_by: 7`, 2}, {`20 | divided_by: 7.0`, 2.857142857142857}, + {`20 | divided_by: 's'`, nil}, {`1.2 | round`, 1}, {`2.7 | round`, 3}, @@ -143,6 +150,8 @@ var filterTests = []struct { // Jekyll extensions; added here for convenient testing // TODO add this just to the test environment {`obj | inspect`, `{"a":1}`}, + {`1 | type`, `int`}, + {`"1" | type`, `string`}, } func timeMustParse(s string) time.Time { diff --git a/render/render_test.go b/render/render_test.go index f80c0de..ea39224 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -28,6 +28,7 @@ var renderTests = []struct{ in, out string }{ {`{{ "abc" }}`, "abc"}, {`{{ x }}`, "123"}, {`{{ page.title }}`, "Introduction"}, + {`{{ array }}`, "firstsecondthird"}, {`{{ array[1] }}`, "second"}, }