Skip to content

Commit f1b178d

Browse files
committed
Boolean precedence unit tests
1 parent 1ae2ff1 commit f1b178d

File tree

3 files changed

+233
-56
lines changed

3 files changed

+233
-56
lines changed

test/test_boolean_helper.rb

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
module Minitest
5+
module Assertions
6+
include Liquid
7+
def assert_with_lax_parsing(template, expected_output, context = {})
8+
prev_error_mode = Liquid::Environment.default.error_mode
9+
Liquid::Environment.default.error_mode = :lax
10+
11+
begin
12+
actual_output = Liquid::Template.parse(template).render(context)
13+
rescue StandardError => e
14+
actual_output = e.message
15+
ensure
16+
Liquid::Environment.default.error_mode = prev_error_mode
17+
end
18+
19+
assert_equal(expected_output.strip, actual_output.strip)
20+
end
21+
22+
def assert_parity(liquid_expression, expected_result, args = {})
23+
assert_condition(liquid_expression, expected_result, args)
24+
assert_expression(liquid_expression, expected_result, args)
25+
end
26+
27+
def assert_expression(liquid_expression, expected_result, args = {})
28+
assert_parity_scenario(:expression, "{{ #{liquid_expression} }}", expected_result, args)
29+
end
30+
31+
def assert_condition(liquid_condition, expected_result, args = {})
32+
assert_parity_scenario(:condition, "{% if #{liquid_condition} %}true{% else %}false{% endif %}", expected_result, args)
33+
end
34+
35+
def assert_parity_scenario(kind, template, exp_output, args = {})
36+
act_output = Liquid::Template.parse(template).render(args)
37+
38+
assert_equal(exp_output, act_output, <<~ERROR_MESSAGE)
39+
#{kind.to_s.capitalize} template failure:
40+
---
41+
#{template}
42+
---
43+
args: #{args.inspect}
44+
ERROR_MESSAGE
45+
end
46+
end
47+
end
48+
49+
class LinkDrop < Liquid::Drop
50+
attr_accessor :levels, :links, :title, :type, :url
51+
52+
def initialize(levels: nil, links: nil, title: nil, type: nil, url: nil)
53+
super()
54+
55+
@levels = levels
56+
@links = links
57+
@title = title
58+
@type = type
59+
@url = url
60+
end
61+
end
+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
require 'test_boolean_helper'
5+
6+
class BooleanPrecedenceUnitTest < Minitest::Test
7+
include Liquid
8+
9+
def test_basic_boolean_parenthesized_expressions
10+
assert_parity("false and (false or true)", "false")
11+
assert_parity("true and (false or true)", "true")
12+
assert_parity("(true and false) or true", "true")
13+
assert_parity("(false and true) or false", "false")
14+
end
15+
16+
def test_nested_boolean_parentheses
17+
assert_parity("(false and (true or false)) or true", "true")
18+
assert_parity("true and (false or (true and true))", "true")
19+
assert_parity("(true and (false or false)) or false", "false")
20+
end
21+
22+
def test_multiple_operations_with_consistent_operators
23+
assert_parity("(true and true) and (false or true)", "true")
24+
assert_parity("(false or false) or (true and false)", "false")
25+
end
26+
27+
def test_parentheses_changing_default_precedence
28+
# Default precedence: (true and false) or true
29+
assert_parity("true and false or true", "true")
30+
# With parentheses: true and (false or true)
31+
assert_parity("true and (false or true)", "true")
32+
33+
# Default precedence: false or (true and true)
34+
assert_parity("false or true and true", "true")
35+
# With parentheses: (false or true) and true
36+
assert_parity("(false or true) and true", "true")
37+
end
38+
39+
def test_boolean_parentheses_with_variables
40+
assert_parity("(a or b) and c", "true", { "a" => true, "b" => false, "c" => true })
41+
assert_parity("(a or b) and c", "false", { "a" => true, "b" => false, "c" => false })
42+
assert_parity("a and (b or c)", "true", { "a" => true, "b" => false, "c" => true })
43+
assert_parity("a and (b or c)", "false", { "a" => false, "b" => true, "c" => true })
44+
end
45+
46+
def test_comparison_operators_inside_parentheses
47+
assert_parity("(1 > 0) and (2 < 3)", "true")
48+
assert_parity("(1 < 0) or (2 > 3)", "false")
49+
assert_parity("true and (1 == 1)", "true")
50+
assert_parity("false or (2 != 2)", "false")
51+
end
52+
53+
def test_complex_nested_boolean_expressions
54+
assert_parity("((true and false) or (false and true)) or ((false or true) and (true or false))", "true")
55+
assert_parity("((true and true) or (false and false)) and ((true or false) and (false or true))", "true")
56+
end
57+
58+
def test_not_operator_with_parentheses
59+
# Testing how 'not' interacts with parentheses
60+
assert_parity("not (true or false)", "false")
61+
assert_parity("not (false and true)", "true")
62+
assert_parity("(not false) and true", "true")
63+
assert_parity("(not true) or false", "false")
64+
assert_parity("not (not true)", "true")
65+
end
66+
67+
def test_nil_values_with_boolean_precedence
68+
# How nil values interact with boolean expressions and parentheses
69+
assert_parity("nil and (true or false)", "false")
70+
assert_parity("(nil or true) and false", "false")
71+
assert_parity("(nil and nil) or true", "true")
72+
assert_parity("true and (nil or false)", "false")
73+
end
74+
75+
def test_mixed_primitive_types_with_parentheses
76+
# Testing how different types interact in boolean expressions with parentheses
77+
assert_parity("('' or 0) and true", "true")
78+
assert_parity("(true and 'string') or false", "true")
79+
assert_parity("(false or '') and 1", "false")
80+
assert_parity("(nil or false) and 'text'", "false")
81+
end
82+
83+
def test_triple_operator_precedence
84+
# Testing three different operators with different parenthesizing
85+
assert_parity("true or false and true or false", "true") # default precedence
86+
assert_parity("true or (false and true) or false", "true")
87+
assert_parity("(true or false) and (true or false)", "true")
88+
assert_parity("((true or false) and true) or false", "true")
89+
assert_parity("true or (false and (true or false))", "true")
90+
end
91+
92+
def test_undefined_variables_with_parentheses
93+
# How undefined variables behave with parentheses
94+
assert_parity("(undefined_var or true) and false", "false")
95+
assert_parity("true and (undefined_var or false)", "false")
96+
assert_parity("(undefined_var and true) or true", "true")
97+
assert_parity("false or (undefined_var and false)", "false")
98+
end
99+
100+
def test_comparison_chaining_with_parentheses
101+
# Testing how comparison chains work with parentheses
102+
assert_parity("(1 < 2) and (2 < 3) and (3 < 4)", "true")
103+
assert_parity("(1 < 2) and ((2 > 3) or (3 < 4))", "true")
104+
assert_parity(
105+
"(a > b) or ((c < d) and (e == f))",
106+
"true",
107+
{ "a" => 5, "b" => 3, "c" => 1, "d" => 2, "e" => 7, "f" => 7 },
108+
)
109+
assert_parity(
110+
"(a > b) or ((c < d) and (e == f))",
111+
"false",
112+
{ "a" => 3, "b" => 5, "c" => 2, "d" => 1, "e" => 7, "f" => 8 },
113+
)
114+
end
115+
116+
def test_deeply_nested_expressions
117+
# Testing very deep nesting to ensure parser handles it correctly
118+
assert_parity("(((true and true) or (false and false)) and ((true or false) and (true)))", "true")
119+
assert_parity(
120+
"(((a or b) and c) or (d and (e or f)))",
121+
"true",
122+
{ "a" => false, "b" => true, "c" => true, "d" => true, "e" => true, "f" => false },
123+
)
124+
end
125+
126+
def test_malformed_parentheses
127+
# Unbalanced parentheses - missing closing parenthesis
128+
template = "{% if (true and false %}true{% else %}false{% endif %}"
129+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
130+
131+
# Unbalanced parentheses - missing opening parenthesis
132+
template = "{% if true and false) %}true{% else %}false{% endif %}"
133+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
134+
135+
# Empty parentheses
136+
template = "{% if () %}true{% else %}false{% endif %}"
137+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
138+
139+
# Consecutive opening parentheses without operators
140+
template = "{% if ((true) %}true{% else %}false{% endif %}"
141+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
142+
143+
# Consecutive closing parentheses without proper opening
144+
template = "{% if (true)) %}true{% else %}false{% endif %}"
145+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
146+
147+
# Parentheses with missing operand
148+
template = "{% if (and true) %}true{% else %}false{% endif %}"
149+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
150+
151+
# Operator followed immediately by closing parenthesis
152+
template = "{% if (true and) %}true{% else %}false{% endif %}"
153+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
154+
155+
# Nested malformed parentheses
156+
template = "{% if (true and (false or true) %}true{% else %}false{% endif %}"
157+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
158+
159+
# Double parentheses with no content between them
160+
template = "{% if true and (()) %}true{% else %}false{% endif %}"
161+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
162+
163+
# Misplaced parentheses around operators
164+
template = "{% if true (and) false %}true{% else %}false{% endif %}"
165+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
166+
167+
# Parentheses at wrong position in expression
168+
template = "{% if true) and (false %}true{% else %}false{% endif %}"
169+
assert_raises(Liquid::SyntaxError) { Liquid::Template.parse(template) }
170+
end
171+
end

test/unit/boolean_unit_test.rb

+1-56
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'test_helper'
4+
require 'test_boolean_helper'
45

56
class BooleanUnitTest < Minitest::Test
67
include Liquid
@@ -487,60 +488,4 @@ def test_assign_boolean_expression_to_variable
487488
template = Liquid::Template.parse("{% assign is_preview_mode = content_for_header contains 'foo' or content_for_header contains 'bar' %}{{ is_preview_mode }}")
488489
assert_equal("false", template.render(context))
489490
end
490-
491-
private
492-
493-
def assert_with_lax_parsing(template, expected_output, context = {})
494-
prev_error_mode = Liquid::Environment.default.error_mode
495-
Liquid::Environment.default.error_mode = :lax
496-
497-
begin
498-
actual_output = Liquid::Template.parse(template).render(context)
499-
rescue StandardError => e
500-
actual_output = e.message
501-
ensure
502-
Liquid::Environment.default.error_mode = prev_error_mode
503-
end
504-
505-
assert_equal(expected_output.strip, actual_output.strip)
506-
end
507-
508-
def assert_parity(liquid_expression, expected_result, args = {})
509-
assert_condition(liquid_expression, expected_result, args)
510-
assert_expression(liquid_expression, expected_result, args)
511-
end
512-
513-
def assert_expression(liquid_expression, expected_result, args = {})
514-
assert_parity_scenario(:expression, "{{ #{liquid_expression} }}", expected_result, args)
515-
end
516-
517-
def assert_condition(liquid_condition, expected_result, args = {})
518-
assert_parity_scenario(:condition, "{% if #{liquid_condition} %}true{% else %}false{% endif %}", expected_result, args)
519-
end
520-
521-
def assert_parity_scenario(kind, template, exp_output, args = {})
522-
act_output = Liquid::Template.parse(template).render(args)
523-
524-
assert_equal(exp_output, act_output, <<~ERROR_MESSAGE)
525-
#{kind.to_s.capitalize} template failure:
526-
---
527-
#{template}
528-
---
529-
args: #{args.inspect}
530-
ERROR_MESSAGE
531-
end
532-
533-
class LinkDrop < Liquid::Drop
534-
attr_accessor :levels, :links, :title, :type, :url
535-
536-
def initialize(levels: nil, links: nil, title: nil, type: nil, url: nil)
537-
super()
538-
539-
@levels = levels
540-
@links = links
541-
@title = title
542-
@type = type
543-
@url = url
544-
end
545-
end
546491
end

0 commit comments

Comments
 (0)