Refactor exec tests further

This commit is contained in:
Tyler Sommer 2023-03-31 23:53:04 -06:00
parent 22492ee956
commit 6d77def7dd
No known key found for this signature in database
GPG key ID: C09C010500DBD008

View file

@ -10,118 +10,113 @@ import (
"github.com/tyler-sommer/stick/parse"
)
// execTest is an extensible template execution test.
type execTest interface {
name() string
tpl() string
ctx() map[string]Value
expected() expectedChecker
// A testValidator checks that the result of a test execution is as expected.
type testValidator func(actual string, err error) error
// execTest is a configurable template execution test.
type execTest struct {
name string
tpl string
ctx map[string]Value
checkResult testValidator
visitNode func(parse.Node) // When visitNode is set, it will be called after each node is parsed.
}
// _execTest is the standard implementation of execTest.
type _execTest struct {
_name string
_tpl string
_ctx map[string]Value
_expected expectedChecker
type testOption func(t *execTest)
func newExecTest(name string, tpl string, v testValidator, opts ...testOption) execTest {
t := execTest{name: name, tpl: tpl, checkResult: v}
for _, o := range opts {
o(&t)
}
return t
}
func (t _execTest) name() string { return t._name }
func (t _execTest) tpl() string { return t._tpl }
func (t _execTest) ctx() map[string]Value { return t._ctx }
func (t _execTest) expected() expectedChecker { return t._expected }
func newExecTest(name string, tpl string, ctx map[string]Value, expected expectedChecker) execTest {
return _execTest{name, tpl, ctx, expected}
// withContext sets the context variables for the template.
func withContext(ctx map[string]Value) testOption {
return func(t *execTest) {
t.ctx = ctx
}
}
// execTestWithPatch is an execTest with a monkey patch function defined.
type execTestWithPatch struct {
execTest
patchNode func(parse.Node) // When patchNode is set, it will be called during parsing after each node is parsed.
}
// withPatch enhances a test with the ability to monkey patch the parsed nodes.
// withNodeVisitor enhances a test with the ability to inspect parsed nodes.
//
// Patching is used when a test needs to create alter the internal structure of a template.
// This can be used to create an invalid tree that would not normally be parsed, but may be
// necessary to test a certain error condition.
func withPatch(t execTest, patch func(parse.Node)) execTestWithPatch {
return execTestWithPatch{t, patch}
// For the purposes of this testing, this provides the ability to muck around with the internal
// structure of a template. This can be used to create an invalid tree that would not normally
// be parsed, but may be necessary to test a certain error condition.
func withNodeVisitor(visitFunc func(parse.Node)) testOption {
return func(t *execTest) {
t.visitNode = visitFunc
}
}
var tests = []execTest{
newExecTest("Hello, World", "Hello, World!", nil, expect("Hello, World!")),
newExecTest("Hello, Tyler!", "Hello, {{ name }}!", map[string]Value{"name": "Tyler"}, expect("Hello, Tyler!")),
newExecTest("Simple if", `<li class="{% if active %}active{% endif %}">`, map[string]Value{"active": true}, expect(`<li class="active">`)),
newExecTest("Simple inheritance", `{% extends 'Hello, {% block test %}universe{% endblock %}!' %}{% block test %}world{% endblock %}`, nil, expect(`Hello, world!`)),
newExecTest("Simple include", `This is a test. {% include 'Hello, {{ name }}!' %} This concludes the test.`, map[string]Value{"name": "John"}, expect(`This is a test. Hello, John! This concludes the test.`)),
newExecTest("Include with", `{% include 'Hello, {{ name }}{{ value }}' with vars %}`, map[string]Value{"value": "!", "vars": map[string]Value{"name": "Adam"}}, expect(`Hello, Adam!`)),
newExecTest("Include with literal", `{% include 'Hello, {{ name }}{{ value }}' with {"name": "world", "value": "!"} only %}`, nil, expect(`Hello, world!`)),
newExecTest("Embed", `Well. {% embed 'Hello, {% block name %}World{% endblock %}!' %}{% block name %}Tyler{% endblock %}{% endembed %}`, nil, expect(`Well. Hello, Tyler!`)),
newExecTest("Constant null", `{% if test == null %}Yes{% else %}no{% endif %}`, map[string]Value{"test": nil}, expect(`Yes`)),
newExecTest("Constant bool", `{% if test == true %}Yes{% else %}no{% endif %}`, map[string]Value{"test": false}, expect(`no`)),
newExecTest("Chained attributes", `{{ entity.attr.Name }}`, map[string]Value{"entity": map[string]Value{"attr": struct{ Name string }{"Tyler"}}}, expect(`Tyler`)),
newExecTest("Attribute method call", `{{ entity.Name('lower') }}`, map[string]Value{"entity": &fakePerson{"Johnny"}}, expect(`lowerJohnny`)),
newExecTest("For loop", `{% for i in 1..3 %}{{ i }}{% endfor %}`, nil, expect(`123`)),
newExecTest("Hello, World", "Hello, World!", expect("Hello, World!")),
newExecTest("Hello, Tyler!", "Hello, {{ name }}!", expect("Hello, Tyler!"), withContext(map[string]Value{"name": "Tyler"})),
newExecTest("Simple if", `<li class="{% if active %}active{% endif %}">`, expect(`<li class="active">`), withContext(map[string]Value{"active": true})),
newExecTest("Simple inheritance", `{% extends 'Hello, {% block test %}universe{% endblock %}!' %}{% block test %}world{% endblock %}`, expect(`Hello, world!`)),
newExecTest("Simple include", `This is a test. {% include 'Hello, {{ name }}!' %} This concludes the test.`, expect(`This is a test. Hello, John! This concludes the test.`), withContext(map[string]Value{"name": "John"})),
newExecTest("Include with", `{% include 'Hello, {{ name }}{{ value }}' with vars %}`, expect(`Hello, Adam!`), withContext(map[string]Value{"value": "!", "vars": map[string]Value{"name": "Adam"}})),
newExecTest("Include with literal", `{% include 'Hello, {{ name }}{{ value }}' with {"name": "world", "value": "!"} only %}`, expect(`Hello, world!`)),
newExecTest("Embed", `Well. {% embed 'Hello, {% block name %}World{% endblock %}!' %}{% block name %}Tyler{% endblock %}{% endembed %}`, expect(`Well. Hello, Tyler!`)),
newExecTest("Constant null", `{% if test == null %}Yes{% else %}no{% endif %}`, expect(`Yes`), withContext(map[string]Value{"test": nil})),
newExecTest("Constant bool", `{% if test == true %}Yes{% else %}no{% endif %}`, expect(`no`), withContext(map[string]Value{"test": false})),
newExecTest("Chained attributes", `{{ entity.attr.Name }}`, expect(`Tyler`), withContext(map[string]Value{"entity": map[string]Value{"attr": struct{ Name string }{"Tyler"}}})),
newExecTest("Attribute method call", `{{ entity.Name('lower') }}`, expect(`lowerJohnny`), withContext(map[string]Value{"entity": &fakePerson{"Johnny"}})),
newExecTest("For loop", `{% for i in 1..3 %}{{ i }}{% endfor %}`, expect(`123`)),
newExecTest(
"For loop with inner loop",
`{% for i in test %}{% for j in i %}{{ j }}{{ loop.index }}{{ loop.parent.index }}{% if loop.first %},{% endif %}{% if loop.last %};{% endif %}{% endfor %}{% if loop.first %}f{% endif %}{% if loop.last %}l{% endif %}:{% endfor %}`,
map[string]Value{
expect(`111,221331;f:412,522632;:713,823933;l:`),
withContext(map[string]Value{
"test": [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}},
},
expect(`111,221331;f:412,522632;:713,823933;l:`),
}),
),
newExecTest(
"For loop variables",
`{% for i in 1..3 %}{{ i }}{{ loop.index }}{{ loop.index0 }}{{ loop.revindex }}{{ loop.revindex0 }}{{ loop.length }}{% if loop.first %}f{% endif %}{% if loop.last %}l{% endif %}{% endfor %}`,
nil,
expect(`110323f221213332103l`),
),
newExecTest("For else", `{% for i in emptySet %}{{ i }}{% else %}No results.{% endfor %}`, map[string]Value{"emptySet": []int{}}, expect(`No results.`)),
newExecTest("For else", `{% for i in emptySet %}{{ i }}{% else %}No results.{% endfor %}`, expect(`No results.`), withContext(map[string]Value{"emptySet": []int{}})),
newExecTest(
"For map",
`{% for k, v in data %}Record {{ loop.index }}: {{ k }}: {{ v }}{% if not loop.last %} - {% endif %}{% endfor %}`,
map[string]Value{"data": map[string]float64{"Group A": 5.12, "Group B": 5.09}},
expect(`Record 1: Group A: 5.12 - Record 2: Group B: 5.09`, `Record 1: Group B: 5.09 - Record 2: Group A: 5.12`),
withContext(map[string]Value{"data": map[string]float64{"Group A": 5.12, "Group B": 5.09}}),
),
newExecTest(
"Some operators",
`{{ 4.5 * 10 }} - {{ 3 + true }} - {{ 3 + 4 == 7.0 }} - {{ 10 % 2 == 0 }} - {{ 10 ** 2 > 99.9 and 10 ** 2 <= 100 }}`,
nil,
expect(`45 - 4 - 1 - 1 - 1`),
),
newExecTest("In and not in", `{{ 5 in set and 4 not in set }}`, map[string]Value{"set": []int{5, 10}}, expect(`1`)),
newExecTest("Function call", `{{ multiply(num, 5) }}`, map[string]Value{"num": 10}, expect(`50`)),
newExecTest("Filter call", `Welcome, {{ name }}`, nil, expect(`Welcome, `)),
newExecTest("Filter call", `Welcome, {{ name|default('User') }}`, map[string]Value{"name": nil}, expect(`Welcome, User`)),
newExecTest("Filter call", `Welcome, {{ surname|default('User') }}`, map[string]Value{"name": nil}, expect(`Welcome, User`)),
newExecTest("In and not in", `{{ 5 in set and 4 not in set }}`, expect(`1`), withContext(map[string]Value{"set": []int{5, 10}})),
newExecTest("Function call", `{{ multiply(num, 5) }}`, expect(`50`), withContext(map[string]Value{"num": 10})),
newExecTest("Filter call", `Welcome, {{ name }}`, expect(`Welcome, `)),
newExecTest("Filter call", `Welcome, {{ name|default('User') }}`, expect(`Welcome, User`), withContext(map[string]Value{"name": nil})),
newExecTest("Filter call", `Welcome, {{ surname|default('User') }}`, expect(`Welcome, User`), withContext(map[string]Value{"name": nil})),
newExecTest(
"Basic use statement",
`{% extends '{% block message %}{% endblock %}' %}{% use '{% block message %}Hello{% endblock %}' %}`,
nil,
expect("Hello"),
),
newExecTest(
"Extended use statement",
`{% extends '{% block message %}{% endblock %}' %}{% use '{% block message %}Hello{% endblock %}' with message as base_message %}{% block message %}{{ block('base_message') }}, World!{% endblock %}`,
nil,
expect("Hello, World!"),
),
newExecTest(
"Set statement",
`{% set val = 'a value' %}{{ val }}`,
nil,
expect("a value"),
),
newExecTest(
"Set statement with body",
`{% set var1 = 'Hello,' %}{% set var2 %}{{ var1 }} World!{% endset %}{{ var2 }}`,
nil,
expect("Hello, World!"),
),
newExecTest(
@ -135,107 +130,107 @@ var tests = []execTest{
{{ var0 }}
{{ var2 }}
{{ var0 }}`,
nil,
expectContains("Hello, World!\n1\n\n\nHello, World!\n1"),
),
withPatch(newExecTest(
newExecTest(
"Set statement invalid expr type",
`{% set v = 10 %}`,
nil,
expectErrorContains("unable to evaluate unsupported Expr type: *parse.TextNode"),
), func(n parse.Node) {
if sn, ok := n.(*parse.SetNode); ok {
sn.X = parse.NewTextNode("Hello", sn.X.Start())
}
}),
withNodeVisitor(func(n parse.Node) {
if sn, ok := n.(*parse.SetNode); ok {
sn.X = parse.NewTextNode("Hello", sn.X.Start())
}
}),
),
newExecTest(
"Do statement",
`{% do p.Name('Mister ') %}{{ p.Name('') }}`,
map[string]Value{"p": &fakePerson{"Meeseeks"}},
expect("Mister Meeseeks"),
withContext(map[string]Value{"p": &fakePerson{"Meeseeks"}}),
),
newExecTest(
"Filter statement",
`{% filter upper %}hello, world!{% endfilter %}`,
nil,
expect("HELLO, WORLD!"),
),
newExecTest(
"Import statement",
`{% import 'macros.twig' as mac %}{{ mac.test("hi") }}`,
nil,
expect("test: hi"),
),
newExecTest(
"From statement",
`{% from 'macros.twig' import test, def as other %}{{ other("", "HI!") }}`,
nil,
expect("HI!"),
),
newExecTest(
"Ternary if",
`{{ false ? (true ? "Hello" : "World") : "Words" }}`,
nil,
expect("Words"),
),
newExecTest(
"Hash literal",
`{{ {"test": 1}["test"] }}`,
nil,
expect("1"),
),
newExecTest(
"Another hash literal",
`{% set v = {quadruple: "to the power of four!", 0: "ew", "0": "it's not that bad"} %}ew? {{ v.0 }} {{ v.quadruple }}`,
nil,
expect("ew? it's not that bad to the power of four!"),
),
newExecTest(
"Array literal",
`{{ ["test", 1, "bar"][2] }}`,
nil,
expect("bar"),
),
newExecTest(
"Another Array literal",
`{{ ["test", 1, "bar"].1 }}`,
nil,
expect("1"),
),
newExecTest(
"Comparison with or",
`{% if item1 == "banana" or item2 == "apple" %}At least one item is correct{% else %}neither item is correct{% endif %}`,
map[string]Value{"item1": "orange", "item2": "apple"},
expect("At least one item is correct"),
withContext(map[string]Value{"item1": "orange", "item2": "apple"}),
),
newExecTest(
"Non-existent map element without default",
`{{ data.A }} {{ data.NotThere }} {{ data.B }}`,
map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}},
expect("Foo Bar"),
withContext(map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}}),
),
newExecTest(
"Non-existent map element with default",
`{{ data.A }} {{ data.NotThere|default("default value") }} {{ data.B }}`,
map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}},
expect("Foo default value Bar"),
withContext(map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}}),
),
newExecTest(
"Accessing templateName on _self",
`Template: {{ _self.templateName }}`,
nil,
expect("Template: Template: {{ _self.templateName }}"),
),
withPatch(_execTest{
newExecTest(
"Unsupported binary operator",
`{{ 1 + 2 }}`,
nil,
expectErrorContains("unsupported binary operator: _"),
}, func(n parse.Node) {
if bn, ok := n.(*parse.BinaryExpr); ok {
bn.Op = "_"
}
}),
withNodeVisitor(func(n parse.Node) {
if bn, ok := n.(*parse.BinaryExpr); ok {
bn.Op = "_"
}
}),
),
newExecTest(
"Unsupported binary operator",
`{{ 1 + 2 }}`,
expectErrorContains("unable to evaluate unsupported Expr type: *parse.TextNode"),
withNodeVisitor(func(n parse.Node) {
if bn, ok := n.(*parse.BinaryExpr); ok {
bn.Right = parse.NewTextNode("foo", bn.Right.Start())
}
}),
),
}
func joinExpected(expected []string) string {
@ -277,7 +272,7 @@ func (err *expectErrorMismatchError) Error() string {
if len(err.expected) == 0 {
if err.actual == nil {
// shouldn't happen in practice, but technically possible
return fmt.Sprint("expected error mismatch but there was no actual error and no error was expected! (bug?)")
return fmt.Sprint("error mismatch expected but there was no actual error and no error was expected! (bug?)")
}
return fmt.Sprintf("unexpected error %#v", err.actual.Error())
}
@ -292,9 +287,7 @@ func newExpectErrorMismatchError(actual error, expected ...string) error {
return &expectErrorMismatchError{actual, expected}
}
type expectedChecker func(actual string, err error) error
func expectChained(expects ...expectedChecker) expectedChecker {
func expectChained(expects ...testValidator) testValidator {
return func(actual string, err error) error {
for _, expect := range expects {
if e := expect(actual, err); e != nil {
@ -305,7 +298,7 @@ func expectChained(expects ...expectedChecker) expectedChecker {
}
}
func expect(expected ...string) expectedChecker {
func expect(expected ...string) testValidator {
return expectChained(expectNoError(), func(actual string, err error) error {
for _, exp := range expected {
if actual == exp {
@ -316,7 +309,7 @@ func expect(expected ...string) expectedChecker {
})
}
func expectContains(matches string) expectedChecker {
func expectContains(matches string) testValidator {
return expectChained(expectNoError(), func(actual string, err error) error {
if !strings.Contains(actual, matches) {
return newExpectFailedError(true, actual, matches)
@ -325,7 +318,7 @@ func expectContains(matches string) expectedChecker {
})
}
func expectNoError() expectedChecker {
func expectNoError() testValidator {
return func(actual string, err error) error {
if err != nil {
return newExpectErrorMismatchError(err)
@ -334,7 +327,7 @@ func expectNoError() expectedChecker {
}
}
func expectErrorContains(expected ...string) expectedChecker {
func expectErrorContains(expected ...string) testValidator {
return func(_ string, err error) error {
if err == nil && len(expected) == 0 {
return nil
@ -354,17 +347,6 @@ func expectErrorContains(expected ...string) expectedChecker {
}
}
func evaluateTest(t *testing.T, env *Env, test execTest) {
w := &bytes.Buffer{}
err := execute(test.tpl(), w, test.ctx(), env)
check := test.expected()
out := w.String()
if err := check(out, err); err != nil {
t.Errorf("%s: %s", test.name(), err)
}
}
type testLoader struct {
templates map[string]Template
}
@ -402,16 +384,29 @@ func (t *testLoader) Load(name string) (Template, error) {
return tpl(name, name), nil
}
// nodeMonkeyPatcher is a parse.NodeVisitor to enable tests to arbitrarily modify a parsed tree.
type nodeMonkeyPatcher struct {
patch func(parse.Node)
// testVisitor is a parse.NodeVisitor to enable tests to arbitrarily modify a parsed tree.
type testVisitor struct {
// visit is called when the visitor leaves a node
visit func(parse.Node)
}
func (v *nodeMonkeyPatcher) Enter(parse.Node) {}
func (v *testVisitor) Enter(parse.Node) {
// only Leave is used for simplicity's sake
}
func (v *nodeMonkeyPatcher) Leave(n parse.Node) {
if v.patch != nil {
v.patch(n)
func (v *testVisitor) Leave(n parse.Node) {
if v.visit != nil {
v.visit(n)
}
}
func evaluateTest(t *testing.T, env *Env, test execTest) {
w := &bytes.Buffer{}
err := execute(test.tpl, w, test.ctx, env)
out := w.String()
if err := test.checkResult(out, err); err != nil {
t.Errorf("%s: %s", test.name, err)
}
}
@ -446,13 +441,10 @@ func TestExec(t *testing.T) {
}
return val
}
patcher := &nodeMonkeyPatcher{}
env.Visitors = append(env.Visitors, patcher)
tv := &testVisitor{}
env.Visitors = append(env.Visitors, tv)
for _, test := range tests {
patcher.patch = nil
if t, ok := test.(execTestWithPatch); ok {
patcher.patch = t.patchNode
}
tv.visit = test.visitNode
evaluateTest(t, env, test)
}
}