liquid-c 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/liquid.yml +24 -2
  3. data/.gitignore +4 -0
  4. data/.rubocop.yml +14 -0
  5. data/Gemfile +14 -5
  6. data/README.md +29 -5
  7. data/Rakefile +13 -62
  8. data/ext/liquid_c/block.c +488 -60
  9. data/ext/liquid_c/block.h +28 -2
  10. data/ext/liquid_c/c_buffer.c +42 -0
  11. data/ext/liquid_c/c_buffer.h +76 -0
  12. data/ext/liquid_c/context.c +233 -0
  13. data/ext/liquid_c/context.h +70 -0
  14. data/ext/liquid_c/document_body.c +89 -0
  15. data/ext/liquid_c/document_body.h +59 -0
  16. data/ext/liquid_c/expression.c +116 -0
  17. data/ext/liquid_c/expression.h +24 -0
  18. data/ext/liquid_c/extconf.rb +19 -9
  19. data/ext/liquid_c/intutil.h +22 -0
  20. data/ext/liquid_c/lexer.c +6 -2
  21. data/ext/liquid_c/lexer.h +18 -3
  22. data/ext/liquid_c/liquid.c +76 -6
  23. data/ext/liquid_c/liquid.h +24 -1
  24. data/ext/liquid_c/parse_context.c +76 -0
  25. data/ext/liquid_c/parse_context.h +13 -0
  26. data/ext/liquid_c/parser.c +141 -65
  27. data/ext/liquid_c/parser.h +4 -2
  28. data/ext/liquid_c/raw.c +110 -0
  29. data/ext/liquid_c/raw.h +6 -0
  30. data/ext/liquid_c/resource_limits.c +279 -0
  31. data/ext/liquid_c/resource_limits.h +23 -0
  32. data/ext/liquid_c/stringutil.h +44 -0
  33. data/ext/liquid_c/tokenizer.c +149 -35
  34. data/ext/liquid_c/tokenizer.h +20 -9
  35. data/ext/liquid_c/usage.c +18 -0
  36. data/ext/liquid_c/usage.h +9 -0
  37. data/ext/liquid_c/variable.c +196 -20
  38. data/ext/liquid_c/variable.h +18 -1
  39. data/ext/liquid_c/variable_lookup.c +44 -0
  40. data/ext/liquid_c/variable_lookup.h +8 -0
  41. data/ext/liquid_c/vm.c +588 -0
  42. data/ext/liquid_c/vm.h +25 -0
  43. data/ext/liquid_c/vm_assembler.c +491 -0
  44. data/ext/liquid_c/vm_assembler.h +240 -0
  45. data/ext/liquid_c/vm_assembler_pool.c +97 -0
  46. data/ext/liquid_c/vm_assembler_pool.h +27 -0
  47. data/lib/liquid/c/compile_ext.rb +44 -0
  48. data/lib/liquid/c/version.rb +3 -1
  49. data/lib/liquid/c.rb +225 -46
  50. data/liquid-c.gemspec +16 -10
  51. data/performance/c_profile.rb +23 -0
  52. data/performance.rb +6 -4
  53. data/rakelib/compile.rake +15 -0
  54. data/rakelib/integration_test.rake +43 -0
  55. data/rakelib/performance.rake +43 -0
  56. data/rakelib/rubocop.rake +6 -0
  57. data/rakelib/unit_test.rake +14 -0
  58. data/test/integration_test.rb +11 -0
  59. data/test/liquid_test_helper.rb +21 -0
  60. data/test/test_helper.rb +14 -2
  61. data/test/unit/block_test.rb +130 -0
  62. data/test/unit/context_test.rb +83 -0
  63. data/test/unit/expression_test.rb +186 -0
  64. data/test/unit/gc_stress_test.rb +28 -0
  65. data/test/unit/raw_test.rb +19 -0
  66. data/test/unit/resource_limits_test.rb +50 -0
  67. data/test/unit/tokenizer_test.rb +90 -20
  68. data/test/unit/variable_test.rb +212 -60
  69. metadata +59 -11
  70. 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
@@ -1,41 +1,111 @@
1
1
  # encoding: utf-8
2
- require 'test_helper'
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 [' '], tokenize(' ')
7
- assert_equal ['hello world'], tokenize('hello world')
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 ['{{funk}}'], tokenize('{{funk}}')
12
- assert_equal [' ', '{{funk}}', ' '], tokenize(' {{funk}} ')
13
- assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')
14
- assert_equal [' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')
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 ['{%comment%}'], tokenize('{%comment%}')
19
- assert_equal [' ', '{%comment%}', ' '], tokenize(' {%comment%} ')
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
- assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')
22
- assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")
43
+ # Strips whitespace
44
+ assert_equal(["", "funk", "", "so | brother"], tokenize(source, for_liquid_tag: true, trimmed: true))
23
45
  end
24
46
 
25
- def test_utf8_encoded_template
26
- source = 'auswählen'
27
- assert_equal Encoding::UTF_8, source.encoding
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 [Encoding::UTF_8], output.map(&:encoding)
30
- assert_equal [source], output
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 tokenize(source)
36
- tokenizer = Liquid::C::Tokenizer.new(source, false)
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