liquid-c 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/liquid.yml +24 -2
- data/.gitignore +4 -0
- data/.rubocop.yml +14 -0
- data/Gemfile +14 -5
- data/README.md +29 -5
- data/Rakefile +13 -62
- data/ext/liquid_c/block.c +488 -60
- data/ext/liquid_c/block.h +28 -2
- data/ext/liquid_c/c_buffer.c +42 -0
- data/ext/liquid_c/c_buffer.h +76 -0
- data/ext/liquid_c/context.c +233 -0
- data/ext/liquid_c/context.h +70 -0
- data/ext/liquid_c/document_body.c +89 -0
- data/ext/liquid_c/document_body.h +59 -0
- data/ext/liquid_c/expression.c +116 -0
- data/ext/liquid_c/expression.h +24 -0
- data/ext/liquid_c/extconf.rb +19 -9
- data/ext/liquid_c/intutil.h +22 -0
- data/ext/liquid_c/lexer.c +6 -2
- data/ext/liquid_c/lexer.h +18 -3
- data/ext/liquid_c/liquid.c +76 -6
- data/ext/liquid_c/liquid.h +24 -1
- data/ext/liquid_c/parse_context.c +76 -0
- data/ext/liquid_c/parse_context.h +13 -0
- data/ext/liquid_c/parser.c +141 -65
- data/ext/liquid_c/parser.h +4 -2
- data/ext/liquid_c/raw.c +110 -0
- data/ext/liquid_c/raw.h +6 -0
- data/ext/liquid_c/resource_limits.c +279 -0
- data/ext/liquid_c/resource_limits.h +23 -0
- data/ext/liquid_c/stringutil.h +44 -0
- data/ext/liquid_c/tokenizer.c +149 -35
- data/ext/liquid_c/tokenizer.h +20 -9
- data/ext/liquid_c/usage.c +18 -0
- data/ext/liquid_c/usage.h +9 -0
- data/ext/liquid_c/variable.c +196 -20
- data/ext/liquid_c/variable.h +18 -1
- data/ext/liquid_c/variable_lookup.c +44 -0
- data/ext/liquid_c/variable_lookup.h +8 -0
- data/ext/liquid_c/vm.c +588 -0
- data/ext/liquid_c/vm.h +25 -0
- data/ext/liquid_c/vm_assembler.c +491 -0
- data/ext/liquid_c/vm_assembler.h +240 -0
- data/ext/liquid_c/vm_assembler_pool.c +97 -0
- data/ext/liquid_c/vm_assembler_pool.h +27 -0
- data/lib/liquid/c/compile_ext.rb +44 -0
- data/lib/liquid/c/version.rb +3 -1
- data/lib/liquid/c.rb +225 -46
- data/liquid-c.gemspec +16 -10
- data/performance/c_profile.rb +23 -0
- data/performance.rb +6 -4
- data/rakelib/compile.rake +15 -0
- data/rakelib/integration_test.rake +43 -0
- data/rakelib/performance.rake +43 -0
- data/rakelib/rubocop.rake +6 -0
- data/rakelib/unit_test.rake +14 -0
- data/test/integration_test.rb +11 -0
- data/test/liquid_test_helper.rb +21 -0
- data/test/test_helper.rb +14 -2
- data/test/unit/block_test.rb +130 -0
- data/test/unit/context_test.rb +83 -0
- data/test/unit/expression_test.rb +186 -0
- data/test/unit/gc_stress_test.rb +28 -0
- data/test/unit/raw_test.rb +19 -0
- data/test/unit/resource_limits_test.rb +50 -0
- data/test/unit/tokenizer_test.rb +90 -20
- data/test/unit/variable_test.rb +212 -60
- metadata +59 -11
- data/test/liquid_test.rb +0 -11
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class BlockTest < MiniTest::Test
|
6
|
+
def test_no_allocation_of_trimmed_strings
|
7
|
+
template = Liquid::Template.parse("{{ a -}} {{- b }}")
|
8
|
+
assert_equal(2, template.root.nodelist.size)
|
9
|
+
|
10
|
+
template = Liquid::Template.parse("{{ a -}} foo {{- b }}")
|
11
|
+
assert_equal(3, template.root.nodelist.size)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_raise_on_output_with_non_utf8_encoding
|
15
|
+
output = String.new(encoding: Encoding::ASCII)
|
16
|
+
template = Liquid::Template.parse("ascii text")
|
17
|
+
exc = assert_raises(Encoding::CompatibilityError) do
|
18
|
+
template.render!({}, output: output)
|
19
|
+
end
|
20
|
+
assert_equal("non-UTF8 encoded output (US-ASCII) not supported", exc.message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_write_unicode_characters
|
24
|
+
output = String.new(encoding: Encoding::UTF_8)
|
25
|
+
template = Liquid::Template.parse("ü{{ unicode_char }}")
|
26
|
+
assert_equal("üñ", template.render!({ "unicode_char" => "ñ" }, output: output))
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_op_write_raw_w
|
30
|
+
source = "a" * 2**8
|
31
|
+
template = Liquid::Template.parse(source)
|
32
|
+
assert_equal(source, template.render!)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_raise_for_non_c_parse_context
|
36
|
+
parse_context = Liquid::ParseContext.new
|
37
|
+
assert_raises(RuntimeError) do
|
38
|
+
Liquid::C::BlockBody.new(parse_context)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Test for bug: https://github.com/Shopify/liquid-c/pull/120
|
43
|
+
def test_bug_120_instrument
|
44
|
+
calls = []
|
45
|
+
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
|
46
|
+
Liquid::Template.parse("{{ -.1 }}")
|
47
|
+
end
|
48
|
+
assert_equal(["liquid_c_negative_float_without_integer"], calls)
|
49
|
+
|
50
|
+
calls = []
|
51
|
+
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
|
52
|
+
Liquid::Template.parse("{{ .1 }}")
|
53
|
+
end
|
54
|
+
assert_equal([], calls)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_disassemble_raw_w
|
58
|
+
source = "a" * 2**8
|
59
|
+
template = Liquid::Template.parse(source)
|
60
|
+
block_body = template.root.body
|
61
|
+
assert_equal(<<~ASM, block_body.disassemble)
|
62
|
+
0x0000: write_raw_w("#{source}")
|
63
|
+
0x0104: leave
|
64
|
+
ASM
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_disassemble
|
68
|
+
source = <<~LIQUID
|
69
|
+
raw
|
70
|
+
{{- var | default: "none", allow_false: true -}}
|
71
|
+
{%- increment counter -%}
|
72
|
+
LIQUID
|
73
|
+
template = Liquid::Template.parse(source, line_numbers: true)
|
74
|
+
block_body = template.root.body
|
75
|
+
increment_node = block_body.nodelist[2]
|
76
|
+
assert_instance_of(Liquid::Increment, increment_node)
|
77
|
+
assert_equal(<<~ASM, block_body.disassemble)
|
78
|
+
0x0000: write_raw("raw")
|
79
|
+
0x0005: render_variable_rescue(line_number: 2)
|
80
|
+
0x0009: find_static_var("var")
|
81
|
+
0x000c: push_const(\"none\")
|
82
|
+
0x000f: push_const(\"allow_false\")
|
83
|
+
0x0012: push_true
|
84
|
+
0x0013: hash_new(1)
|
85
|
+
0x0015: builtin_filter(name: :default, num_args: 3)
|
86
|
+
0x0018: pop_write
|
87
|
+
0x0019: write_node(#{increment_node.inspect})
|
88
|
+
0x001c: leave
|
89
|
+
ASM
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_exception_renderer_exception
|
93
|
+
original_error = Liquid::Error.new("original")
|
94
|
+
handler_error = RuntimeError.new("exception handler error")
|
95
|
+
context = Liquid::Context.new("raise_error" => ->(_ctx) { raise(original_error) })
|
96
|
+
context.exception_renderer = lambda do |exc|
|
97
|
+
if exc == original_error
|
98
|
+
raise(handler_error)
|
99
|
+
end
|
100
|
+
exc
|
101
|
+
end
|
102
|
+
template = Liquid::Template.parse("{% assign x = raise_error %}")
|
103
|
+
exc = assert_raises(RuntimeError) do
|
104
|
+
template.render(context)
|
105
|
+
end
|
106
|
+
assert_equal(handler_error, exc)
|
107
|
+
end
|
108
|
+
|
109
|
+
StubFileSystem = Struct.new(:partials) do
|
110
|
+
def read_template_file(template_path)
|
111
|
+
partials.fetch(template_path)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_include_partial_with_syntax_error
|
116
|
+
old_file_system = Liquid::Template.file_system
|
117
|
+
begin
|
118
|
+
Liquid::Template.file_system = StubFileSystem.new({
|
119
|
+
"invalid" => "{% foo %}",
|
120
|
+
"valid" => '{% include "nested" %}',
|
121
|
+
"nested" => "valid",
|
122
|
+
})
|
123
|
+
|
124
|
+
template = Liquid::Template.parse("{% include 'invalid' %},{% include 'valid' %}")
|
125
|
+
assert_equal("Liquid syntax error: Unknown tag 'foo',valid", template.render)
|
126
|
+
ensure
|
127
|
+
Liquid::Template.file_system = old_file_system
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
require "bigdecimal"
|
5
|
+
|
6
|
+
class ContextTest < Minitest::Test
|
7
|
+
def test_evaluate_works_with_normal_values
|
8
|
+
context = Liquid::Context.new
|
9
|
+
|
10
|
+
["abc", 123, false, 1.21, BigDecimal(42)].each do |value|
|
11
|
+
assert_equal(value, context.evaluate(value))
|
12
|
+
end
|
13
|
+
|
14
|
+
assert_nil(context.evaluate(nil))
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_evaluate_works_with_classes_that_have_an_evaluate_method
|
18
|
+
class_with_evaluate = Class.new do
|
19
|
+
def evaluate(_context)
|
20
|
+
42
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
assert_equal(42, Liquid::Context.new.evaluate(class_with_evaluate.new))
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_evaluate_works_with_variable_lookup
|
28
|
+
assert_equal(42, Liquid::Context.new({ "var" => 42 }).evaluate(Liquid::C::Expression.strict_parse("var")))
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_evaluating_a_variable_entirely_within_c
|
32
|
+
context = Liquid::Context.new({ "var" => 42 })
|
33
|
+
lookup = Liquid::C::Expression.strict_parse("var")
|
34
|
+
context.evaluate(lookup) # memoize vm_internal_new calls
|
35
|
+
|
36
|
+
called_ruby_method_count = 0
|
37
|
+
called_c_method_count = 0
|
38
|
+
|
39
|
+
test_thread = Thread.current
|
40
|
+
begin
|
41
|
+
call_trace = TracePoint.trace(:call) do |t|
|
42
|
+
unless t.self == TracePoint || t.self.is_a?(TracePoint) || Thread.current != test_thread
|
43
|
+
called_ruby_method_count += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
c_call_trace = TracePoint.trace(:c_call) do |t|
|
48
|
+
unless t.self == TracePoint || t.self.is_a?(TracePoint) || Thread.current != test_thread
|
49
|
+
called_c_method_count += 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context.evaluate(lookup)
|
54
|
+
ensure
|
55
|
+
call_trace&.disable
|
56
|
+
c_call_trace&.disable
|
57
|
+
end
|
58
|
+
|
59
|
+
assert_equal(0, called_ruby_method_count)
|
60
|
+
assert_equal(1, called_c_method_count) # context.evaluate call
|
61
|
+
end
|
62
|
+
|
63
|
+
class TestDrop < Liquid::Drop
|
64
|
+
def is_filtering # rubocop:disable Naming/PredicateName
|
65
|
+
@context.send(:c_filtering?)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_c_filtering_predicate
|
70
|
+
context = Liquid::Context.new({ "test" => [TestDrop.new] })
|
71
|
+
template = Liquid::Template.parse('{{ test[0].is_filtering }},{{ test | map: "is_filtering" }}')
|
72
|
+
|
73
|
+
assert_equal("false,true", template.render!(context))
|
74
|
+
assert_equal(false, context.send(:c_filtering?))
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_strict_variables=
|
78
|
+
context = Liquid::Context.new
|
79
|
+
assert_equal(false, context.strict_variables)
|
80
|
+
context.strict_variables = true
|
81
|
+
assert_equal(true, context.strict_variables)
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ExpressionTest < MiniTest::Test
|
6
|
+
def test_constant_literals
|
7
|
+
assert_equal(true, Liquid::C::Expression.strict_parse("true"))
|
8
|
+
assert_equal(false, Liquid::C::Expression.strict_parse("false"))
|
9
|
+
assert_nil(Liquid::C::Expression.strict_parse("nil"))
|
10
|
+
assert_nil(Liquid::C::Expression.strict_parse("null"))
|
11
|
+
|
12
|
+
empty = Liquid::C::Expression.strict_parse("empty")
|
13
|
+
assert_equal("", empty)
|
14
|
+
assert_same(empty, Liquid::C::Expression.strict_parse("blank"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_push_literals
|
18
|
+
assert_nil(compile_and_eval("nil"))
|
19
|
+
assert_equal(true, compile_and_eval("true"))
|
20
|
+
assert_equal(false, compile_and_eval("false"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_constant_integer
|
24
|
+
assert_equal(42, Liquid::C::Expression.strict_parse("42"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_push_int8
|
28
|
+
assert_equal(127, compile_and_eval("127"))
|
29
|
+
assert_equal(-128, compile_and_eval("-128"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_push_int16
|
33
|
+
assert_equal(128, compile_and_eval("128"))
|
34
|
+
assert_equal(-129, compile_and_eval("-129"))
|
35
|
+
assert_equal(32767, compile_and_eval("32767"))
|
36
|
+
assert_equal(-32768, compile_and_eval("-32768"))
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_push_large_fixnum
|
40
|
+
assert_equal(32768, compile_and_eval("32768"))
|
41
|
+
assert_equal(-2147483648, compile_and_eval("-2147483648"))
|
42
|
+
assert_equal(2147483648, compile_and_eval("2147483648"))
|
43
|
+
assert_equal(4611686018427387903, compile_and_eval("4611686018427387903"))
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_push_big_int
|
47
|
+
num = 1 << 128
|
48
|
+
assert_equal(num, compile_and_eval(num.to_s))
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_float
|
52
|
+
assert_equal(123.4, Liquid::C::Expression.strict_parse("123.4"))
|
53
|
+
assert_equal(-1.5, compile_and_eval("-1.5"))
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_string
|
57
|
+
assert_equal("hello", Liquid::C::Expression.strict_parse('"hello"'))
|
58
|
+
assert_equal("world", compile_and_eval("'world'"))
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_find_static_variable
|
62
|
+
context = Liquid::Context.new({ "x" => 123 })
|
63
|
+
expr = Liquid::C::Expression.strict_parse("x")
|
64
|
+
|
65
|
+
assert_instance_of(Liquid::C::Expression, expr)
|
66
|
+
assert_equal(123, context.evaluate(expr))
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_find_dynamic_variable
|
70
|
+
context = Liquid::Context.new({ "x" => "y", "y" => 42 })
|
71
|
+
expr = Liquid::C::Expression.strict_parse("[x]")
|
72
|
+
assert_equal(42, context.evaluate(expr))
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_find_missing_variable
|
76
|
+
context = Liquid::Context.new({})
|
77
|
+
expr = Liquid::C::Expression.strict_parse("missing")
|
78
|
+
|
79
|
+
assert_nil(context.evaluate(expr))
|
80
|
+
|
81
|
+
context.strict_variables = true
|
82
|
+
|
83
|
+
assert_raises(Liquid::UndefinedVariable) do
|
84
|
+
context.evaluate(expr)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_lookup_const_key
|
89
|
+
context = Liquid::Context.new({ "obj" => { "prop" => "some value" } })
|
90
|
+
|
91
|
+
expr = Liquid::C::Expression.strict_parse("obj.prop")
|
92
|
+
assert_equal("some value", context.evaluate(expr))
|
93
|
+
|
94
|
+
expr = Liquid::C::Expression.strict_parse('obj["prop"]')
|
95
|
+
assert_equal("some value", context.evaluate(expr))
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_lookup_variable_key
|
99
|
+
context = Liquid::Context.new({ "field_name" => "prop", "obj" => { "prop" => "another value" } })
|
100
|
+
expr = Liquid::C::Expression.strict_parse("obj[field_name]")
|
101
|
+
assert_equal("another value", context.evaluate(expr))
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_lookup_command
|
105
|
+
context = Liquid::Context.new({ "ary" => ["a", "b", "c"] })
|
106
|
+
assert_equal(3, context.evaluate(Liquid::C::Expression.strict_parse("ary.size")))
|
107
|
+
assert_equal("a", context.evaluate(Liquid::C::Expression.strict_parse("ary.first")))
|
108
|
+
assert_equal("c", context.evaluate(Liquid::C::Expression.strict_parse("ary.last")))
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_lookup_missing_key
|
112
|
+
context = Liquid::Context.new({ "obj" => {} })
|
113
|
+
expr = Liquid::C::Expression.strict_parse("obj.missing")
|
114
|
+
|
115
|
+
assert_nil(context.evaluate(expr))
|
116
|
+
|
117
|
+
context.strict_variables = true
|
118
|
+
|
119
|
+
assert_raises(Liquid::UndefinedVariable) do
|
120
|
+
context.evaluate(expr)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_lookup_on_var_with_literal_name
|
125
|
+
context = Liquid::Context.new({ "blank" => { "x" => "result" } })
|
126
|
+
|
127
|
+
assert_equal("result", context.evaluate(Liquid::C::Expression.strict_parse("blank.x")))
|
128
|
+
assert_equal("result", context.evaluate(Liquid::C::Expression.strict_parse('blank["x"]')))
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_const_range
|
132
|
+
assert_equal((1..2), Liquid::C::Expression.strict_parse("(1..2)"))
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_dynamic_range
|
136
|
+
context = Liquid::Context.new({ "var" => 42 })
|
137
|
+
expr = Liquid::C::Expression.strict_parse("(1..var)")
|
138
|
+
assert_instance_of(Liquid::C::Expression, expr)
|
139
|
+
assert_equal((1..42), context.evaluate(expr))
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_disassemble
|
143
|
+
expression = Liquid::C::Expression.strict_parse("foo.bar[123]")
|
144
|
+
assert_equal(<<~ASM, expression.disassemble)
|
145
|
+
0x0000: find_static_var("foo")
|
146
|
+
0x0003: lookup_const_key("bar")
|
147
|
+
0x0006: push_int8(123)
|
148
|
+
0x0008: lookup_key
|
149
|
+
0x0009: leave
|
150
|
+
ASM
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_disassemble_int16
|
154
|
+
assert_equal(<<~ASM, Liquid::C::Expression.strict_parse("[12345]").disassemble)
|
155
|
+
0x0000: push_int16(12345)
|
156
|
+
0x0003: find_var
|
157
|
+
0x0004: leave
|
158
|
+
ASM
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_disable_c_nodes
|
162
|
+
context = Liquid::Context.new({ "x" => 123 })
|
163
|
+
|
164
|
+
expr = Liquid::ParseContext.new.parse_expression("x")
|
165
|
+
assert_instance_of(Liquid::C::Expression, expr)
|
166
|
+
assert_equal(123, context.evaluate(expr))
|
167
|
+
|
168
|
+
expr = Liquid::ParseContext.new(disable_liquid_c_nodes: true).parse_expression("x")
|
169
|
+
assert_instance_of(Liquid::VariableLookup, expr)
|
170
|
+
assert_equal(123, context.evaluate(expr))
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
class ReturnKeyDrop < Liquid::Drop
|
176
|
+
def liquid_method_missing(key)
|
177
|
+
key
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def compile_and_eval(source)
|
182
|
+
context = Liquid::Context.new({ "ret_key" => ReturnKeyDrop.new })
|
183
|
+
expr = Liquid::C::Expression.strict_parse("ret_key[#{source}]")
|
184
|
+
context.evaluate(expr)
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "test_helper"
|
5
|
+
|
6
|
+
# Help catch bugs from objects not being marked at all
|
7
|
+
# GC opportunities.
|
8
|
+
class GCStressTest < Minitest::Test
|
9
|
+
def test_compile_and_render
|
10
|
+
source = "{% assign x = 1 %}{% if x -%} x: {{ x | plus: 2 }}{% endif %}"
|
11
|
+
result = gc_stress do
|
12
|
+
Liquid::Template.parse(source).render!
|
13
|
+
end
|
14
|
+
assert_equal("x: 3", result)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def gc_stress
|
20
|
+
old_value = GC.stress
|
21
|
+
GC.stress = true
|
22
|
+
begin
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
GC.stress = old_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class RawTest < Minitest::Test
|
6
|
+
class RawWrapper < Liquid::Raw
|
7
|
+
def render_to_output_buffer(_context, output)
|
8
|
+
output << "<"
|
9
|
+
super
|
10
|
+
output << ">"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
Liquid::Template.register_tag("raw_wrapper", RawWrapper)
|
14
|
+
|
15
|
+
def test_derived_class
|
16
|
+
output = Liquid::Template.parse("{% raw_wrapper %}body{% endraw_wrapper %}").render!
|
17
|
+
assert_equal("<body>", output)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ResourceLimitsTest < Minitest::Test
|
6
|
+
def test_increment_render_score
|
7
|
+
resource_limits = Liquid::ResourceLimits.new(render_score_limit: 5)
|
8
|
+
resource_limits.increment_render_score(4)
|
9
|
+
assert_raises(Liquid::MemoryError) do
|
10
|
+
resource_limits.increment_render_score(2)
|
11
|
+
end
|
12
|
+
assert_equal(6, resource_limits.render_score)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_increment_assign_score
|
16
|
+
resource_limits = Liquid::ResourceLimits.new(assign_score_limit: 5)
|
17
|
+
resource_limits.increment_assign_score(5)
|
18
|
+
assert_raises(Liquid::MemoryError) do
|
19
|
+
resource_limits.increment_assign_score(1)
|
20
|
+
end
|
21
|
+
assert_equal(6, resource_limits.assign_score)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_increment_write_score
|
25
|
+
resource_limits = Liquid::ResourceLimits.new(render_length_limit: 5)
|
26
|
+
output = "a" * 10
|
27
|
+
assert_raises(Liquid::MemoryError) do
|
28
|
+
resource_limits.increment_write_score(output)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_raise_limits_reached
|
33
|
+
resource_limits = Liquid::ResourceLimits.new({})
|
34
|
+
assert_raises(Liquid::MemoryError) do
|
35
|
+
resource_limits.raise_limits_reached
|
36
|
+
end
|
37
|
+
assert(resource_limits.reached?)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_with_capture
|
41
|
+
resource_limits = Liquid::ResourceLimits.new(assign_score_limit: 5)
|
42
|
+
output = "foo"
|
43
|
+
|
44
|
+
resource_limits.with_capture do
|
45
|
+
resource_limits.increment_write_score(output)
|
46
|
+
end
|
47
|
+
|
48
|
+
assert_equal(3, resource_limits.assign_score)
|
49
|
+
end
|
50
|
+
end
|
data/test/unit/tokenizer_test.rb
CHANGED
@@ -1,41 +1,111 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "test_helper"
|
5
|
+
|
6
|
+
class TokenizerTest < Minitest::Test
|
7
|
+
def test_tokenizer_nil
|
8
|
+
tokenizer = new_tokenizer(nil)
|
9
|
+
assert_nil(tokenizer.send(:shift))
|
10
|
+
end
|
3
11
|
|
4
|
-
class TokenizerTest < MiniTest::Unit::TestCase
|
5
12
|
def test_tokenize_strings
|
6
|
-
assert_equal
|
7
|
-
assert_equal
|
13
|
+
assert_equal([" "], tokenize(" "))
|
14
|
+
assert_equal(["hello world"], tokenize("hello world"))
|
8
15
|
end
|
9
16
|
|
10
17
|
def test_tokenize_variables
|
11
|
-
assert_equal
|
12
|
-
assert_equal
|
13
|
-
assert_equal
|
14
|
-
assert_equal
|
18
|
+
assert_equal(["{{funk}}"], tokenize("{{funk}}"))
|
19
|
+
assert_equal([" ", "{{funk}}", " "], tokenize(" {{funk}} "))
|
20
|
+
assert_equal([" ", "{{funk}}", " ", "{{so}}", " ", "{{brother}}", " "], tokenize(" {{funk}} {{so}} {{brother}} "))
|
21
|
+
assert_equal([" ", "{{ funk }}", " "], tokenize(" {{ funk }} "))
|
22
|
+
|
23
|
+
# Doesn't strip whitespace
|
24
|
+
assert_equal([" ", " funk ", " "], tokenize(" {{ funk }} ", trimmed: true))
|
15
25
|
end
|
16
26
|
|
17
27
|
def test_tokenize_blocks
|
18
|
-
assert_equal
|
19
|
-
assert_equal
|
28
|
+
assert_equal(["{%comment%}"], tokenize("{%comment%}"))
|
29
|
+
assert_equal([" ", "{%comment%}", " "], tokenize(" {%comment%} "))
|
30
|
+
|
31
|
+
assert_equal([" ", "{%comment%}", " ", "{%endcomment%}", " "], tokenize(" {%comment%} {%endcomment%} "))
|
32
|
+
assert_equal([" ", "{% comment %}", " ", "{% endcomment %}", " "], tokenize(" {% comment %} {% endcomment %} "))
|
33
|
+
|
34
|
+
# Doesn't strip whitespace
|
35
|
+
assert_equal([" ", " comment ", " "], tokenize(" {% comment %} ", trimmed: true))
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_tokenize_for_liquid_tag
|
39
|
+
source = "\nfunk\n\n so | brother \n"
|
40
|
+
|
41
|
+
assert_equal(["", "funk", "", " so | brother "], tokenize(source, for_liquid_tag: true))
|
20
42
|
|
21
|
-
|
22
|
-
assert_equal
|
43
|
+
# Strips whitespace
|
44
|
+
assert_equal(["", "funk", "", "so | brother"], tokenize(source, for_liquid_tag: true, trimmed: true))
|
23
45
|
end
|
24
46
|
|
25
|
-
def
|
26
|
-
|
27
|
-
assert_equal
|
47
|
+
def test_invalid_tags
|
48
|
+
assert_equal([""], tokenize("{%-%}", trimmed: true))
|
49
|
+
assert_equal([""], tokenize("{{-}}", trimmed: true))
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_utf8_encoded_source
|
53
|
+
source = "auswählen"
|
54
|
+
assert_equal(Encoding::UTF_8, source.encoding)
|
28
55
|
output = tokenize(source)
|
29
|
-
assert_equal
|
30
|
-
assert_equal
|
56
|
+
assert_equal([Encoding::UTF_8], output.map(&:encoding))
|
57
|
+
assert_equal([source], output)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_utf8_compatible_source
|
61
|
+
source = String.new("ascii", encoding: Encoding::ASCII)
|
62
|
+
tokenizer = new_tokenizer(source)
|
63
|
+
output = tokenizer.send(:shift)
|
64
|
+
assert_equal(Encoding::UTF_8, output.encoding)
|
65
|
+
assert_equal(source, output)
|
66
|
+
assert_nil(tokenizer.send(:shift))
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_non_utf8_compatible_source
|
70
|
+
source = "üñicode".dup.force_encoding(Encoding::BINARY) # rubocop:disable Performance/UnfreezeString
|
71
|
+
exc = assert_raises(Encoding::CompatibilityError) do
|
72
|
+
Liquid::C::Tokenizer.new(source, 1, false)
|
73
|
+
end
|
74
|
+
assert_equal("non-UTF8 encoded source (ASCII-8BIT) not supported", exc.message)
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_source_too_large
|
78
|
+
too_large_source = "a" * 2**24
|
79
|
+
max_length_source = too_large_source.chop
|
80
|
+
|
81
|
+
# C safety check
|
82
|
+
err = assert_raises(ArgumentError) do
|
83
|
+
Liquid::C::Tokenizer.new(too_large_source, 1, false)
|
84
|
+
end
|
85
|
+
assert_match(/Source too large, max \d+ bytes/, err.message)
|
86
|
+
|
87
|
+
# ruby patch fallback
|
88
|
+
parse_context = Liquid::ParseContext.new
|
89
|
+
liquid_c_tokenizer = parse_context.new_tokenizer(max_length_source)
|
90
|
+
assert_instance_of(Liquid::C::Tokenizer, liquid_c_tokenizer)
|
91
|
+
refute(parse_context.liquid_c_nodes_disabled?)
|
92
|
+
|
93
|
+
parse_context = Liquid::ParseContext.new
|
94
|
+
fallback_tokenizer = parse_context.new_tokenizer(too_large_source)
|
95
|
+
assert_instance_of(Liquid::Tokenizer, fallback_tokenizer)
|
96
|
+
assert_equal(true, parse_context.liquid_c_nodes_disabled?)
|
31
97
|
end
|
32
98
|
|
33
99
|
private
|
34
100
|
|
35
|
-
def
|
36
|
-
|
101
|
+
def new_tokenizer(source, parse_context: Liquid::ParseContext.new)
|
102
|
+
parse_context.new_tokenizer(source)
|
103
|
+
end
|
104
|
+
|
105
|
+
def tokenize(source, for_liquid_tag: false, trimmed: false)
|
106
|
+
tokenizer = Liquid::C::Tokenizer.new(source, 1, for_liquid_tag)
|
37
107
|
tokens = []
|
38
|
-
while t = tokenizer.shift
|
108
|
+
while (t = trimmed ? tokenizer.send(:shift_trimmed) : tokenizer.send(:shift))
|
39
109
|
tokens << t
|
40
110
|
end
|
41
111
|
tokens
|