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.
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