liquid-c 4.0.1 → 4.2.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +23 -0
  3. data/.github/workflows/liquid.yml +36 -11
  4. data/.gitignore +4 -0
  5. data/.rubocop.yml +14 -0
  6. data/Gemfile +15 -5
  7. data/README.md +32 -8
  8. data/Rakefile +12 -63
  9. data/ext/liquid_c/block.c +493 -60
  10. data/ext/liquid_c/block.h +28 -2
  11. data/ext/liquid_c/c_buffer.c +42 -0
  12. data/ext/liquid_c/c_buffer.h +76 -0
  13. data/ext/liquid_c/context.c +233 -0
  14. data/ext/liquid_c/context.h +70 -0
  15. data/ext/liquid_c/document_body.c +97 -0
  16. data/ext/liquid_c/document_body.h +59 -0
  17. data/ext/liquid_c/expression.c +116 -0
  18. data/ext/liquid_c/expression.h +24 -0
  19. data/ext/liquid_c/extconf.rb +21 -9
  20. data/ext/liquid_c/intutil.h +22 -0
  21. data/ext/liquid_c/lexer.c +39 -3
  22. data/ext/liquid_c/lexer.h +18 -3
  23. data/ext/liquid_c/liquid.c +76 -6
  24. data/ext/liquid_c/liquid.h +24 -1
  25. data/ext/liquid_c/liquid_vm.c +618 -0
  26. data/ext/liquid_c/liquid_vm.h +25 -0
  27. data/ext/liquid_c/parse_context.c +76 -0
  28. data/ext/liquid_c/parse_context.h +13 -0
  29. data/ext/liquid_c/parser.c +153 -65
  30. data/ext/liquid_c/parser.h +4 -2
  31. data/ext/liquid_c/raw.c +136 -0
  32. data/ext/liquid_c/raw.h +6 -0
  33. data/ext/liquid_c/resource_limits.c +279 -0
  34. data/ext/liquid_c/resource_limits.h +23 -0
  35. data/ext/liquid_c/stringutil.h +44 -0
  36. data/ext/liquid_c/tokenizer.c +149 -35
  37. data/ext/liquid_c/tokenizer.h +20 -9
  38. data/ext/liquid_c/usage.c +18 -0
  39. data/ext/liquid_c/usage.h +9 -0
  40. data/ext/liquid_c/variable.c +196 -20
  41. data/ext/liquid_c/variable.h +18 -1
  42. data/ext/liquid_c/variable_lookup.c +44 -0
  43. data/ext/liquid_c/variable_lookup.h +8 -0
  44. data/ext/liquid_c/vm_assembler.c +491 -0
  45. data/ext/liquid_c/vm_assembler.h +240 -0
  46. data/ext/liquid_c/vm_assembler_pool.c +99 -0
  47. data/ext/liquid_c/vm_assembler_pool.h +26 -0
  48. data/lib/liquid/c/compile_ext.rb +44 -0
  49. data/lib/liquid/c/version.rb +3 -1
  50. data/lib/liquid/c.rb +226 -48
  51. data/liquid-c.gemspec +16 -10
  52. data/performance/c_profile.rb +23 -0
  53. data/performance.rb +6 -4
  54. data/rakelib/compile.rake +15 -0
  55. data/rakelib/integration_test.rake +43 -0
  56. data/rakelib/performance.rake +43 -0
  57. data/rakelib/rubocop.rake +6 -0
  58. data/rakelib/unit_test.rake +14 -0
  59. data/test/integration_test.rb +11 -0
  60. data/test/liquid_test_helper.rb +21 -0
  61. data/test/test_helper.rb +21 -2
  62. data/test/unit/block_test.rb +137 -0
  63. data/test/unit/context_test.rb +85 -0
  64. data/test/unit/expression_test.rb +191 -0
  65. data/test/unit/gc_stress_test.rb +28 -0
  66. data/test/unit/raw_test.rb +93 -0
  67. data/test/unit/resource_limits_test.rb +50 -0
  68. data/test/unit/tokenizer_test.rb +90 -20
  69. data/test/unit/variable_test.rb +279 -60
  70. metadata +60 -11
  71. data/test/liquid_test.rb +0 -11
@@ -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.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.shift
64
+ assert_equal(Encoding::UTF_8, output.encoding)
65
+ assert_equal(source, output)
66
+ assert_nil(tokenizer.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.shift)
39
109
  tokens << t
40
110
  end
41
111
  tokens
@@ -1,110 +1,329 @@
1
1
  # encoding: utf-8
2
- require 'test_helper'
2
+ # frozen_string_literal: true
3
3
 
4
- class VariableTest < MiniTest::Unit::TestCase
4
+ require "test_helper"
5
+
6
+ class VariableTest < Minitest::Test
5
7
  def test_variable_parse
6
- assert_equal [lookup('hello'), []], variable_parse('hello')
7
- assert_equal ['world', []], variable_parse(' "world" ')
8
- assert_equal [lookup('hello["world"]'), []], variable_parse(' hello["world"] ')
9
- assert_equal [nil, []], variable_parse('')
10
- assert_equal [lookup('question?'), []], variable_parse('question?')
11
- assert_equal [lookup('[meta]'), []], variable_parse('[meta]')
12
- assert_equal [lookup('a-b'), []], variable_parse('a-b')
13
- assert_equal [lookup('a-2'), []], variable_parse('a-2')
8
+ assert_equal("world", variable_strict_parse("hello").render!({ "hello" => "world" }))
9
+ assert_equal("world", variable_strict_parse('"world"').render!)
10
+ assert_equal("answer", variable_strict_parse('hello["world"]').render!({ "hello" => { "world" => "answer" } }))
11
+ assert_equal("answer", variable_strict_parse("question?").render!({ "question?" => "answer" }))
12
+ assert_equal("value", variable_strict_parse("[meta]").render!({ "meta" => "key", "key" => "value" }))
13
+ assert_equal("result", variable_strict_parse("a-b").render!({ "a-b" => "result" }))
14
+ assert_equal("result", variable_strict_parse("a-2").render!({ "a-2" => "result" }))
14
15
  end
15
16
 
16
17
  def test_strictness
17
- assert_raises(Liquid::SyntaxError) { variable_parse(' hello["world\']" ') }
18
- assert_raises(Liquid::SyntaxError) { variable_parse('-..') }
19
- assert_raises(Liquid::SyntaxError) { variable_parse('question?mark') }
20
- assert_raises(Liquid::SyntaxError) { variable_parse('123.foo') }
21
- assert_raises(Liquid::SyntaxError) { variable_parse(' | nothing') }
22
-
23
- ['a .b', 'a. b', 'a . b'].each do |var|
24
- assert_raises(Liquid::SyntaxError) { variable_parse(var) }
25
- end
18
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse(' hello["world\']" ') }
19
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse(" -..") }
20
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse("question?mark") }
21
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse("123.foo") }
22
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse(" | nothing") }
26
23
 
27
- ['a -b', 'a- b', 'a - b'].each do |var|
28
- assert_raises(Liquid::SyntaxError) { variable_parse(var) }
24
+ ["a -b", "a- b", "a - b"].each do |var|
25
+ assert_raises(Liquid::SyntaxError) { variable_strict_parse(var) }
29
26
  end
30
27
  end
31
28
 
32
29
  def test_literals
33
- assert_equal [true, []], variable_parse('true')
34
- assert_equal [nil, []], variable_parse('nil')
35
- assert_equal [123.4, []], variable_parse('123.4')
30
+ assert_equal("", variable_strict_parse("").render!)
31
+ assert_equal("true", variable_strict_parse("true").render!)
32
+ assert_equal("", variable_strict_parse("nil").render!)
33
+ assert_equal("123.4", variable_strict_parse("123.4").render!)
34
+
35
+ assert_equal("blank_value", variable_strict_parse("[blank]").render!({ "" => "blank_value" }))
36
+ assert_equal("result", variable_strict_parse("[true][blank]").render!({ true => { "" => "result" } }))
37
+ assert_equal("result", variable_strict_parse('x["size"]').render!({ "x" => { "size" => "result" } }))
38
+ assert_equal("result", variable_strict_parse("blank.x").render!({ "blank" => { "x" => "result" } }))
39
+ assert_equal("result", variable_strict_parse('blank["x"]').render!({ "blank" => { "x" => "result" } }))
40
+ end
41
+
42
+ module InspectCallFilters
43
+ def filter1(input, *args)
44
+ inspect_call(__method__, input, args)
45
+ end
46
+
47
+ def filter2(input, *args)
48
+ inspect_call(__method__, input, args)
49
+ end
36
50
 
37
- assert_equal [lookup('[blank]'), []], variable_parse('[blank]')
38
- assert_equal [lookup(false, true, [Liquid::Expression::LITERALS['blank']], 0), []], variable_parse('[true][blank]')
39
- assert_equal [lookup('[true][blank]'), []], variable_parse('[true][blank]')
40
- assert_equal [lookup('x["size"]'), []], variable_parse('x["size"]')
51
+ private
52
+
53
+ def inspect_call(filter_name, input, args)
54
+ "{ filter: #{filter_name.inspect}, input: #{input.inspect}, args: #{args.inspect} }"
55
+ end
41
56
  end
42
57
 
43
58
  def test_variable_filter
44
- name = lookup('name')
45
- assert_equal [name, [['filter', []]]], variable_parse(' name | filter ')
46
- assert_equal [name, [['filter1', []], ['filter2', []]]], variable_parse(' name | filter1 | filter2 ')
59
+ context = { "name" => "Bob" }
60
+
61
+ filter1_output = variable_strict_parse("name | filter1").render!(context, filters: [InspectCallFilters])
62
+ assert_equal('{ filter: :filter1, input: "Bob", args: [] }', filter1_output)
63
+
64
+ filter2_output = variable_strict_parse("name | filter1 | filter2").render!(context, filters: [InspectCallFilters])
65
+ assert_equal("{ filter: :filter2, input: #{filter1_output.inspect}, args: [] }", filter2_output)
47
66
  end
48
67
 
49
68
  def test_variable_filter_args
50
- name = lookup('name')
51
- abc = lookup('abc')
69
+ context = { "name" => "Bob", "abc" => "xyz" }
70
+ render_opts = { filters: [InspectCallFilters] }
71
+
72
+ filter1_output = variable_strict_parse("name | filter1: abc").render!(context, render_opts)
73
+ assert_equal('{ filter: :filter1, input: "Bob", args: ["xyz"] }', filter1_output)
52
74
 
53
- assert_equal [name, [['filter', [abc]]]], variable_parse(' name | filter: abc ')
75
+ filter2_output = variable_strict_parse("name | filter1: abc | filter2: abc").render!(context, render_opts)
76
+ assert_equal("{ filter: :filter2, input: #{filter1_output.inspect}, args: [\"xyz\"] }", filter2_output)
54
77
 
55
- assert_equal [name, [['filter1', [abc]], ['filter2', [abc]]]],
56
- variable_parse(' name | filter1: abc | filter2: abc ')
78
+ context = { "name" => "Bob", "a" => 1, "c" => 3, "e" => 5 }
57
79
 
58
- assert_equal [name, [['filter', [lookup('a')], {'b' => lookup('c'), 'd' => lookup('e')}]]],
59
- variable_parse('name | filter : a , b : c , d : e')
80
+ output = variable_strict_parse("name | filter1 : a , b : c , d : e").render!(context, render_opts)
81
+ assert_equal('{ filter: :filter1, input: "Bob", args: [1, {"b"=>3, "d"=>5}] }', output)
60
82
 
61
- assert_raises Liquid::SyntaxError do
62
- variable_parse('name | filter : a : b : c : d : e')
83
+ assert_raises(Liquid::SyntaxError) do
84
+ variable_strict_parse("name | filter : a : b : c : d : e")
63
85
  end
64
86
  end
65
87
 
66
88
  def test_unicode_strings
67
- assert_equal ['å߀êùidhtлsԁѵ߀ráƙìstɦeƅêstpcmáѕterrãcêcհèrr', []],
68
- variable_parse('"å߀êùidhtлsԁѵ߀ráƙìstɦeƅêstpcmáѕterrãcêcհèrr"')
89
+ string_content = "å߀êùidhtлsԁѵ߀ráƙìstɦeƅêstpcmáѕterrãcêcհèrr"
90
+ assert_equal(string_content, variable_strict_parse("\"#{string_content}\"").render!)
69
91
  end
70
92
 
71
93
  def test_broken_unicode_errors
72
94
  err = assert_raises(Liquid::SyntaxError) do
73
95
  Liquid::Template.parse("test {{ \xC2\xA0 test }}", error_mode: :strict)
74
96
  end
75
- assert err.message
97
+ assert(err.message)
76
98
  end
77
99
 
78
100
  def test_callbacks
79
- variable_parses = 0
80
101
  variable_fallbacks = 0
81
102
 
82
103
  callbacks = {
83
- variable_parse: lambda { variable_parses += 1 },
84
- variable_fallback: lambda { variable_fallbacks += 1 }
104
+ variable_fallback: lambda { variable_fallbacks += 1 },
85
105
  }
86
106
 
87
- create_variable('abc', error_mode: :lax, stats_callbacks: callbacks)
88
- assert_equal 1, variable_parses
89
- assert_equal 0, variable_fallbacks
107
+ Liquid::Template.parse("{{abc}}", error_mode: :lax, stats_callbacks: callbacks)
108
+ assert_equal(0, variable_fallbacks)
90
109
 
91
- create_variable('@!#', error_mode: :lax, stats_callbacks: callbacks)
92
- assert_equal 2, variable_parses
93
- assert_equal 1, variable_fallbacks
110
+ Liquid::Template.parse("{{@!#}}", error_mode: :lax, stats_callbacks: callbacks)
111
+ assert_equal(1, variable_fallbacks)
94
112
  end
95
113
 
96
- private
114
+ def test_write_string
115
+ output = Liquid::Template.parse("{{ str }}").render({ "str" => "foo" })
116
+ assert_equal("foo", output)
117
+ end
118
+
119
+ def test_write_fixnum
120
+ output = Liquid::Template.parse("{{ num }}").render({ "num" => 123456 })
121
+ assert_equal("123456", output)
122
+ end
97
123
 
98
- def create_variable(markup, options={})
99
- Liquid::Variable.new(markup, Liquid::ParseContext.new(options))
124
+ def test_write_array
125
+ output = Liquid::Template.parse("{{ ary }}").render({ "ary" => ["foo", 123, ["nested", "ary"], nil, 0.5] })
126
+ assert_equal("foo123nestedary0.5", output)
100
127
  end
101
128
 
102
- def variable_parse(markup)
103
- name = Liquid::Variable.c_strict_parse(markup, filters = [])
104
- [name, filters]
129
+ def test_write_nil
130
+ output = Liquid::Template.parse("{{ obj }}").render({ "obj" => nil })
131
+ assert_equal("", output)
105
132
  end
106
133
 
107
- def lookup(*args)
108
- Liquid::VariableLookup.new(*args)
134
+ class StringConvertible
135
+ def initialize(as_string)
136
+ @as_string = as_string
137
+ end
138
+
139
+ def to_s
140
+ @as_string
141
+ end
142
+
143
+ def to_liquid
144
+ self
145
+ end
146
+ end
147
+
148
+ def test_write_to_s_convertible_object
149
+ output = Liquid::Template.parse("{{ obj }}").render!({ "obj" => StringConvertible.new("foo") })
150
+ assert_equal("foo", output)
151
+ end
152
+
153
+ def test_write_object_with_broken_to_s
154
+ template = Liquid::Template.parse("{{ obj }}")
155
+ exc = assert_raises(TypeError) do
156
+ template.render!({ "obj" => StringConvertible.new(123) })
157
+ end
158
+ assert_equal(
159
+ "VariableTest::StringConvertible#to_s returned a non-String convertible value of type Integer",
160
+ exc.message
161
+ )
162
+ end
163
+
164
+ class DerivedString < String
165
+ def to_s
166
+ self
167
+ end
168
+ end
169
+
170
+ def test_write_derived_string
171
+ output = Liquid::Template.parse("{{ obj }}").render!({ "obj" => DerivedString.new("bar") })
172
+ assert_equal("bar", output)
173
+ end
174
+
175
+ def test_filter_without_args
176
+ output = Liquid::Template.parse("{{ var | upcase }}").render({ "var" => "Hello" })
177
+ assert_equal("HELLO", output)
178
+ end
179
+
180
+ def test_filter_with_const_arg
181
+ output = Liquid::Template.parse("{{ x | plus: 2 }}").render({ "x" => 3 })
182
+ assert_equal("5", output)
183
+ end
184
+
185
+ def test_filter_with_variable_arg
186
+ output = Liquid::Template.parse("{{ x | plus: y }}").render({ "x" => 10, "y" => 123 })
187
+ assert_equal("133", output)
188
+ end
189
+
190
+ def test_filter_with_variable_arg_after_const_arg
191
+ output = Liquid::Template.parse("{{ ary | slice: 1, 2 }}").render({ "ary" => [1, 2, 3, 4] })
192
+ assert_equal("23", output)
193
+ end
194
+
195
+ def test_filter_with_const_keyword_arg
196
+ output = Liquid::Template.parse("{{ value | default: 'None' }}").render({ "value" => false })
197
+ assert_equal("None", output)
198
+
199
+ output = Liquid::Template.parse("{{ value | default: 'None', allow_false: true }}").render({ "value" => false })
200
+ assert_equal("false", output)
201
+ end
202
+
203
+ def test_filter_with_variable_keyword_arg
204
+ template = Liquid::Template.parse("{{ value | default: 'None', allow_false: false_allowed }}")
205
+
206
+ assert_equal("None", template.render({ "value" => false, "false_allowed" => false }))
207
+ assert_equal("false", template.render({ "value" => false, "false_allowed" => true }))
208
+ end
209
+
210
+ def test_filter_error
211
+ output = Liquid::Template.parse("before ({{ ary | concat: 2 }}) after").render({ "ary" => [1] })
212
+ assert_equal("before (Liquid error: concat filter requires an array argument) after", output)
213
+ end
214
+
215
+ def test_render_variable_object
216
+ variable = Liquid::Variable.new("ary | concat: ary2", Liquid::ParseContext.new)
217
+ assert_instance_of(Liquid::C::VariableExpression, variable.name)
218
+
219
+ context = Liquid::Context.new("ary" => [1], "ary2" => [2])
220
+ assert_equal([1, 2], variable.render(context))
221
+
222
+ context["ary2"] = 2
223
+ exc = assert_raises(Liquid::ArgumentError) do
224
+ variable.render(context)
225
+ end
226
+ assert_equal("Liquid error: concat filter requires an array argument", exc.message)
227
+ end
228
+
229
+ def test_filter_argument_error_translation
230
+ variable = Liquid::Variable.new("'some words' | split", Liquid::ParseContext.new)
231
+ context = Liquid::Context.new
232
+ exc = assert_raises(Liquid::ArgumentError) { variable.render(context) }
233
+ assert_equal("Liquid error: wrong number of arguments (given 1, expected 2)", exc.message)
234
+ end
235
+
236
+ class IntegerDrop < Liquid::Drop
237
+ def initialize(value)
238
+ super()
239
+ @value = value.to_i
240
+ end
241
+
242
+ def to_liquid_value
243
+ @value
244
+ end
245
+ end
246
+
247
+ def test_to_liquid_value_on_variable_lookup
248
+ context = {
249
+ "number" => IntegerDrop.new("1"),
250
+ "list" => [1, 2, 3, 4, 5],
251
+ }
252
+
253
+ output = variable_strict_parse("list[number]").render!(context)
254
+ assert_equal("2", output)
255
+ end
256
+
257
+ def test_encoding_error_message_with_multi_byte_characters
258
+ # 2 byte character
259
+ exc = assert_raises(Liquid::SyntaxError) do
260
+ variable_strict_parse("\u00A0")
261
+ end
262
+ assert_equal(
263
+ "Liquid syntax error: Unexpected character \u00A0 in \"{{\u00a0}}\"",
264
+ exc.message
265
+ )
266
+
267
+ # 3 byte character
268
+ exc = assert_raises(Liquid::SyntaxError) do
269
+ variable_strict_parse("\u3042")
270
+ end
271
+ assert_equal(
272
+ "Liquid syntax error: Unexpected character \u3042 in \"{{\u3042}}\"",
273
+ exc.message
274
+ )
275
+
276
+ # 4 byte character
277
+ exc = assert_raises(Liquid::SyntaxError) do
278
+ variable_strict_parse("\u{1F600}")
279
+ end
280
+ assert_equal(
281
+ "Liquid syntax error: Unexpected character \u{1F600} in \"{{\u{1F600}}}\"",
282
+ exc.message
283
+ )
284
+ end
285
+
286
+ def test_invalid_utf8_sequence
287
+ # 2 byte character with 1 byte missing
288
+ exc = assert_raises(ArgumentError) do
289
+ variable_strict_parse("\xC0")
290
+ end
291
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
292
+
293
+ # 3 byte character with 1 byte missing
294
+ exc = assert_raises(ArgumentError) do
295
+ variable_strict_parse("\xE0\x01")
296
+ end
297
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
298
+
299
+ # 3 byte character with 2 byte missing
300
+ exc = assert_raises(ArgumentError) do
301
+ variable_strict_parse("\xE0")
302
+ end
303
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
304
+
305
+ # 4 byte character with 1 byte missing
306
+ exc = assert_raises(ArgumentError) do
307
+ variable_strict_parse("\xF0\x01\x01")
308
+ end
309
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
310
+
311
+ # 4 byte character with 2 byte missing
312
+ exc = assert_raises(ArgumentError) do
313
+ variable_strict_parse("\xF0\x01")
314
+ end
315
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
316
+
317
+ # 4 byte character with 3 byte missing
318
+ exc = assert_raises(ArgumentError) do
319
+ variable_strict_parse("\xF0")
320
+ end
321
+ assert_equal("invalid byte sequence in UTF-8", exc.message)
322
+ end
323
+
324
+ private
325
+
326
+ def variable_strict_parse(markup)
327
+ Liquid::Template.parse("{{#{markup}}}", error_mode: :strict)
109
328
  end
110
329
  end