diff --git a/evaluator/convert.go b/evaluator/convert.go index 7b86527..7711b76 100644 --- a/evaluator/convert.go +++ b/evaluator/convert.go @@ -35,15 +35,17 @@ func conversionError(modifier string, value interface{}, typ reflect.Type) error func Convert(value interface{}, typ reflect.Type) (interface{}, error) { // nolint: gocyclo value = ToLiquid(value) r := reflect.ValueOf(value) - switch { - case typ.Kind() != reflect.String && value != nil && r.Type().ConvertibleTo(typ): - // convert int.Convert(string) yields "\x01" not "1" + // int.Convert(string) returns "\x01" not "1", so guard against that in the following test + if typ.Kind() != reflect.String && value != nil && r.Type().ConvertibleTo(typ) { return r.Convert(typ).Interface(), nil - case typ == timeType && r.Kind() == reflect.String: - return ParseDate(value.(string)) - // case reflect.PtrTo(r.Type()) == typ: - // return &value, nil } + if typ == timeType && r.Kind() == reflect.String { + return ParseDate(value.(string)) + } + // currently unused: + // case reflect.PtrTo(r.Type()) == typ: + // return &value, nil + // } switch typ.Kind() { case reflect.Bool: return !(value == nil || value == false), nil @@ -59,8 +61,7 @@ func Convert(value interface{}, typ reflect.Type) (interface{}, error) { // noli } case reflect.Float32, reflect.Float64: switch value := value.(type) { - case int: - return float64(value), nil + // case int is handled by r.Convert(type) above case string: return strconv.ParseFloat(value, 64) } diff --git a/evaluator/convert_test.go b/evaluator/convert_test.go index 6a5e336..45d53de 100644 --- a/evaluator/convert_test.go +++ b/evaluator/convert_test.go @@ -43,6 +43,13 @@ var convertTests = []struct { // {"March 14, 2016", time.Now(), timeMustParse("2016-03-14T00:00:00Z")}, {redConvertible{}, "", "red"}, } +var convertErrorTests = []struct { + value, proto, expected interface{} +}{ + {map[string]bool{"k": true}, map[int]bool{}, "map key"}, + {map[string]string{"k": "v"}, map[string]int{}, "map value"}, + {map[interface{}]interface{}{"k": "v"}, map[string]int{}, "map value"}, +} func TestConvert(t *testing.T) { for i, test := range convertTests { @@ -55,6 +62,19 @@ func TestConvert(t *testing.T) { }) } } + +func TestConvert_errors(t *testing.T) { + for i, test := range convertErrorTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + typ := reflect.TypeOf(test.proto) + name := fmt.Sprintf("Convert %#v -> %v", test.value, typ) + _, err := Convert(test.value, typ) + require.Errorf(t, err, name) + require.Containsf(t, err.Error(), test.expected, name) + }) + } +} + func TestConvert_map(t *testing.T) { typ := reflect.TypeOf(map[string]string{}) v, err := Convert(map[interface{}]interface{}{"key": "value"}, typ) @@ -64,12 +84,6 @@ func TestConvert_map(t *testing.T) { require.Equal(t, "value", m["key"]) } -func TestConvert_map_key_error(t *testing.T) { - typ := reflect.TypeOf(map[string]int{}) - _, err := Convert(map[interface{}]interface{}{"key": "value"}, typ) - require.Error(t, err) -} - func TestConvert_map_synonym(t *testing.T) { type VariableMap map[interface{}]interface{} typ := reflect.TypeOf(map[string]string{}) diff --git a/expressions/expressions.y b/expressions/expressions.y index d1a12b8..db2dfeb 100644 --- a/expressions/expressions.y +++ b/expressions/expressions.y @@ -1,13 +1,13 @@ %{ package expressions import ( - "fmt" + "fmt" "github.com/osteele/liquid/evaluator" ) func init() { // This allows adding and removing references to fmt in the rules below, - // without having to edit the import statement to avoid erorrs each time. + // without having to comment and un-comment the import statement above. _ = fmt.Sprint("") } diff --git a/render/render_test.go b/render/render_test.go index 06d797e..f80c0de 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -12,23 +12,27 @@ import ( ) func addRenderTestTags(s Config) { - s.AddBlock("err2").Compiler(func(c BlockNode) (func(io.Writer, Context) error, error) { + s.AddBlock("errblock").Compiler(func(c BlockNode) (func(io.Writer, Context) error, error) { return func(w io.Writer, c Context) error { - return fmt.Errorf("stage 2 error") + return fmt.Errorf("errblock error") }, nil }) } var renderTests = []struct{ in, out string }{ + {`{{ nil }}`, ""}, + {`{{ true }}`, "true"}, + {`{{ false }}`, "false"}, {`{{ 12 }}`, "12"}, + {`{{ 12.3 }}`, "12.3"}, + {`{{ "abc" }}`, "abc"}, {`{{ x }}`, "123"}, {`{{ page.title }}`, "Introduction"}, {`{{ array[1] }}`, "second"}, } var renderErrorTests = []struct{ in, out string }{ - // {"{%if syntax error%}{%endif%}", "parse error"}, - {`{% err2 %}{% enderr2 %}`, "stage 2 error"}, + {`{% errblock %}{% enderrblock %}`, "errblock error"}, } var renderTestBindings = map[string]interface{}{ diff --git a/tags/control_flow_tags_test.go b/tags/control_flow_tags_test.go index 37e20ee..66f7209 100644 --- a/tags/control_flow_tags_test.go +++ b/tags/control_flow_tags_test.go @@ -3,6 +3,8 @@ package tags import ( "bytes" "fmt" + "io" + "io/ioutil" "testing" "github.com/osteele/liquid/parser" @@ -43,17 +45,53 @@ var cfTagTests = []struct{ in, expected string }{ {`{% unless false %}true{% endunless %}`, "true"}, } +var cfTagCompilationErrorTests = []struct{ in, expected string }{ + {`{% case syntax error %}{% when 1 %}{% endcase %}`, "syntax error"}, +} + +var cfTagErrorTests = []struct{ in, expected string }{ + {`{% case 1 %}{% when 1 %}{% error %}{% endcase %}`, "tag render error"}, +} + func TestControlFlowTags(t *testing.T) { - config := render.NewConfig() - AddStandardTags(config) + cfg := render.NewConfig() + AddStandardTags(cfg) + for i, test := range cfTagTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { - ast, err := config.Compile(test.in, parser.SourceLoc{}) + root, err := cfg.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) buf := new(bytes.Buffer) - err = render.Render(ast, buf, tagTestBindings, config) + err = render.Render(root, buf, tagTestBindings, cfg) require.NoErrorf(t, err, test.in) require.Equalf(t, test.expected, buf.String(), test.in) }) } } + +func TestControlFlowTags_errors(t *testing.T) { + cfg := render.NewConfig() + AddStandardTags(cfg) + cfg.AddTag("error", func(string) (func(io.Writer, render.Context) error, error) { + return func(io.Writer, render.Context) error { + return fmt.Errorf("tag render error") + }, nil + }) + + for i, test := range cfTagCompilationErrorTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + _, err := cfg.Compile(test.in, parser.SourceLoc{}) + require.Errorf(t, err, test.in) + require.Contains(t, err.Error(), test.expected, test.in) + }) + } + for i, test := range cfTagErrorTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + root, err := cfg.Compile(test.in, parser.SourceLoc{}) + require.NoErrorf(t, err, test.in) + err = render.Render(root, ioutil.Discard, tagTestBindings, cfg) + require.Errorf(t, err, test.in) + require.Contains(t, err.Error(), test.expected, test.in) + }) + } +} diff --git a/tags/iteration_tags_test.go b/tags/iteration_tags_test.go index 555617c..7edc9c4 100644 --- a/tags/iteration_tags_test.go +++ b/tags/iteration_tags_test.go @@ -3,6 +3,7 @@ package tags import ( "bytes" "fmt" + "io/ioutil" "testing" "github.com/osteele/liquid/parser" @@ -10,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -var loopTests = []struct{ in, expected string }{ +var iterationTests = []struct{ in, expected string }{ {`{% for a in array %}{{ a }} {% endfor %}`, "first second third "}, // loop modifiers @@ -69,12 +70,19 @@ var loopTests = []struct{ in, expected string }{ {`{% for i in (3..5) %}{{i}}.{% endfor %}`, "3.4.5."}, } -var loopErrorTests = []struct{ in, expected string }{ - {`{% break %}`, "break outside a loop"}, - {`{% continue %}`, "continue outside a loop"}, +var iterationSyntaxErrorTests = []struct{ in, expected string }{ + {`{% for a b c %}{% endfor %}`, "parse error"}, + {`{% for a in array offset %}{% endfor %}`, "undefined loop modifier"}, + {`{% cycle %}`, "parse error"}, } -var loopTestBindings = map[string]interface{}{ +var iterationErrorTests = []struct{ in, expected string }{ + {`{% break %}`, "break outside a loop"}, + {`{% continue %}`, "continue outside a loop"}, + {`{% cycle 'a', 'b' %}`, "cycle must be within a forloop"}, +} + +var iterationTestBindings = map[string]interface{}{ "array": []string{"first", "second", "third"}, "hash": map[string]interface{}{"a": 1}, } @@ -82,12 +90,12 @@ var loopTestBindings = map[string]interface{}{ func TestLoopTag(t *testing.T) { config := render.NewConfig() AddStandardTags(config) - for i, test := range loopTests { + for i, test := range iterationTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { - ast, err := config.Compile(test.in, parser.SourceLoc{}) + root, err := config.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) buf := new(bytes.Buffer) - err = render.Render(ast, buf, loopTestBindings, config) + err = render.Render(root, buf, iterationTestBindings, config) require.NoErrorf(t, err, test.in) require.Equalf(t, test.expected, buf.String(), test.in) }) @@ -97,12 +105,20 @@ func TestLoopTag(t *testing.T) { func TestLoopTag_errors(t *testing.T) { config := render.NewConfig() AddStandardTags(config) - for i, test := range loopErrorTests { + + for i, test := range iterationSyntaxErrorTests { t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { - ast, err := config.Compile(test.in, parser.SourceLoc{}) + _, err := config.Compile(test.in, parser.SourceLoc{}) + require.Errorf(t, err, test.in) + require.Containsf(t, err.Error(), test.expected, test.in) + }) + } + + for i, test := range iterationErrorTests { + t.Run(fmt.Sprintf("%02d", i+1), func(t *testing.T) { + root, err := config.Compile(test.in, parser.SourceLoc{}) require.NoErrorf(t, err, test.in) - buf := new(bytes.Buffer) - err = render.Render(ast, buf, loopTestBindings, config) + err = render.Render(root, ioutil.Discard, iterationTestBindings, config) require.Errorf(t, err, test.in) require.Containsf(t, err.Error(), test.expected, test.in) })