diff --git a/lib/liquid.rb b/lib/liquid.rb index 445dfbf7..69946a7e 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -80,6 +80,8 @@ require 'liquid/variable_lookup' require 'liquid/range_lookup' require 'liquid/resource_limits' require 'liquid/expression' +require 'liquid/expression/comparison_expression' +require 'liquid/expression/logical_expression' require 'liquid/template' require 'liquid/condition' require 'liquid/utils' @@ -89,4 +91,3 @@ require 'liquid/partial_cache' require 'liquid/usage' require 'liquid/registers' require 'liquid/template_factory' -require 'liquid/boolean_expression' diff --git a/lib/liquid/boolean_expression.rb b/lib/liquid/boolean_expression.rb deleted file mode 100644 index 4dae4fcc..00000000 --- a/lib/liquid/boolean_expression.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -module Liquid - class BooleanExpression - def self.parse(markup, ss = StringScanner.new(""), cache = nil) - markup = markup.strip - - # Handle parenthesized expressions first - if markup.start_with?('(') && balance_parentheses(markup) - # Find the matching closing parenthesis - nesting = 0 - close_index = nil - - markup.chars.each_with_index do |char, i| - if char == '(' - nesting += 1 - elsif char == ')' - nesting -= 1 - if nesting == 0 - close_index = i - break - end - end - end - - if close_index && close_index < markup.length - 1 - # We have something like "(expr) rest" - paren_expr = markup[1...close_index] - rest = markup[close_index + 1..-1].strip - - # Check if rest starts with "and" or "or" (fixed the matching) - if rest =~ /\A(and|or)\s+/i - # Get the operator (and/or) - operator = ::Regexp.last_match(1).downcase - # Get the remaining part after the operator - remaining = rest[operator.length..-1].strip - - left_condition = parse(paren_expr, ss, cache) - right_condition = parse(remaining, ss, cache) - - condition = Condition.new(left_condition, nil, nil) - if operator == 'and' - condition.and(Condition.new(right_condition, nil, nil)) - else # operator == 'or' - condition.or(Condition.new(right_condition, nil, nil)) - end - - return condition - end - elsif close_index == markup.length - 1 - # Just a parenthesized expression "(expr)" - return parse(markup[1...close_index], ss, cache) - end - end - - # Check if we have something like "expr and (expr)" - if (match = markup.match(/\A\s*(.+?)\s+(and|or)\s+\((.+)\)\s*\z/i)) - left_expr = match[1] - operator = match[2].downcase - right_expr = match[3] - - left_condition = parse(left_expr, ss, cache) - right_condition = parse(right_expr, ss, cache) - - condition = Condition.new(left_condition, nil, nil) - if operator == 'and' - condition.and(Condition.new(right_condition, nil, nil)) - else # operator == 'or' - condition.or(Condition.new(right_condition, nil, nil)) - end - - return condition - end - - # First, try to handle OR operator (lower precedence) - if (match = markup.match(/\A\s*(.+?)\s+or\s+(.+)\s*\z/i)) - left = parse(match[1], ss, cache) - right = parse(match[2], ss, cache) - - # Create a condition for OR operation - condition = Condition.new(left, nil, nil) - condition.or(Condition.new(right, nil, nil)) - - return condition - end - - # Then try to handle AND operator (higher precedence) - if (match = markup.match(/\A\s*(.+?)\s+and\s+(.+)\s*\z/i)) - left = parse(match[1], ss, cache) - right = parse(match[2], ss, cache) - - # Create a condition for AND operation - condition = Condition.new(left, nil, nil) - condition.and(Condition.new(right, nil, nil)) - - return condition - end - - # Then try to parse as a comparison expression - if (match = markup.match(/\A\s*(.+?)\s*(==|!=|<>|<=|>=|<|>|contains)\s*(.+)\s*\z/)) - left = Expression.parse(match[1], ss, cache) - operator = match[2] - right = Expression.parse(match[3], ss, cache) - - # Create a condition object to evaluate the expression - condition = Condition.new(left, operator, right) - return condition - end - - # If no operator is found, just parse as regular expression - Expression.parse(markup, ss, cache) - end - - private - - def self.balance_parentheses(markup) - nesting = 0 - markup.each_char do |char| - if char == '(' - nesting += 1 - elsif char == ')' - nesting -= 1 - return false if nesting < 0 # Unbalanced - end - end - nesting == 0 # Should end with balanced parentheses - end - end -end diff --git a/lib/liquid/expression.rb b/lib/liquid/expression.rb index adf340f1..f1293bd7 100644 --- a/lib/liquid/expression.rb +++ b/lib/liquid/expression.rb @@ -51,6 +51,9 @@ module Liquid end def inner_parse(markup, ss, cache) + return LogicalExpression.parse(markup, ss, cache) if LogicalExpression.logical?(markup) + return ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup) + if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX return RangeLookup.parse( Regexp.last_match(1), diff --git a/lib/liquid/expression/comparison_expression.rb b/lib/liquid/expression/comparison_expression.rb new file mode 100644 index 00000000..edbebce2 --- /dev/null +++ b/lib/liquid/expression/comparison_expression.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Liquid + class Expression + class ComparisonExpression + COMPARISON_REGEX = /\A\s*(.+?)\s*(==|!=|<>|<=|>=|<|>|contains)\s*(.+)\s*\z/ + + class << self + def comparison?(markup) + markup =~ COMPARISON_REGEX + end + + def parse(markup, ss, cache) + match = markup.match(COMPARISON_REGEX) + + if match + left = Expression.parse(match[1].strip, ss, cache) + operator = match[2].strip + right = Expression.parse(match[3].strip, ss, cache) + + return Condition.new(left, operator, right) + end + + Condition.new(parse(markup, ss, cache), nil, nil) + end + end + end + end +end diff --git a/lib/liquid/expression/logical_expression.rb b/lib/liquid/expression/logical_expression.rb new file mode 100644 index 00000000..520c8d91 --- /dev/null +++ b/lib/liquid/expression/logical_expression.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Liquid + class Expression + class LogicalExpression + LOGICAL_REGEX = /\A\s*(.+?)\s+(and|or)\s+(.+)\s*\z/i + EXPRESSIONS_AND_OPERATORS = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o + BOOLEAN_OPERATORS = ['and', 'or'].freeze + + class << self + def logical?(markup) + markup =~ LOGICAL_REGEX + end + + def parse(markup, ss, cache) + expressions = markup.scan(EXPRESSIONS_AND_OPERATORS) + + last_expr = expressions.pop + + condition = if ComparisonExpression.comparison?(last_expr) + ComparisonExpression.parse(last_expr, ss, cache) + elsif LOGICAL_REGEX.match?(last_expr) + LogicalExpression.parse(last_expr, ss, cache) + else + Condition.new(Expression.parse(last_expr, ss, cache), nil, nil) + end + + until expressions.empty? + operator = expressions.pop.to_s.strip + next unless BOOLEAN_OPERATORS.include?(operator) + + expr = expressions.pop.to_s.strip + + new_condition = if ComparisonExpression.comparison?(expr) + ComparisonExpression.parse(expr, ss, cache) + elsif LOGICAL_REGEX.match?(expr) + LogicalExpression.parse(expr, ss, cache) + else + Condition.new(Expression.parse(expr, ss, cache), nil, nil) + end + + if operator == 'and' + new_condition.and(condition) + else # operator == 'or' + new_condition.or(condition) + end + + condition = new_condition + end + + condition + end + end + end + end +end diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index 93a0c772..20957065 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -17,8 +17,6 @@ module Liquid FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o JustTagAttributes = /\A#{TagAttributes}\z/o MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om - ComparisonOperator = /==|!=|<>|<=|>=|<|>|contains/o - LogicalOperator = /\s+(and|or)\s+/i attr_accessor :filters, :name, :line_number attr_reader :parse_context @@ -49,14 +47,7 @@ module Liquid name_markup = Regexp.last_match(1) filter_markup = Regexp.last_match(2) - - # Check if name_markup contains a comparison operator or logical operator - @name = if name_markup =~ LogicalOperator || name_markup =~ ComparisonOperator - BooleanExpression.parse(name_markup) - else - parse_context.parse_expression(name_markup) - end - + @name = parse_context.parse_expression(name_markup) if filter_markup =~ FilterMarkupRegex filters = Regexp.last_match(1).scan(FilterParser) filters.each do |f| @@ -74,18 +65,13 @@ module Liquid return if p.look(:end_of_string) - # Check if markup contains a comparison operator or logical operator - if markup =~ LogicalOperator || markup =~ ComparisonOperator - @name = BooleanExpression.parse(markup) - else - @name = parse_context.parse_expression(p.expression) - while p.consume?(:pipe) - filtername = p.consume(:id) - filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY - @filters << parse_filter_expressions(filtername, filterargs) - end - p.consume(:end_of_string) + @name = parse_context.parse_expression(p.expression) + while p.consume?(:pipe) + filtername = p.consume(:id) + filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY + @filters << parse_filter_expressions(filtername, filterargs) end + p.consume(:end_of_string) end def parse_filterargs(p) diff --git a/test/unit/boolean_unit_test.rb b/test/unit/boolean_unit_test.rb index 7e9a2ef9..7e0822de 100644 --- a/test/unit/boolean_unit_test.rb +++ b/test/unit/boolean_unit_test.rb @@ -6,337 +6,146 @@ class BooleanUnitTest < Minitest::Test include Liquid def test_simple_boolean_comparison - template = Liquid::Template.parse("{{ 1 > 0 }}") - assert_equal("true", template.render) + assert_parity("1 > 0", "true") + assert_parity("1 < 0", "false") + end - template = Liquid::Template.parse("{{ 1 < 0 }}") - assert_equal("false", template.render) + def test_boolean_and_operator + assert_parity("true and true", "true") + assert_parity("true and false", "false") + end + + def test_boolean_or_operator + assert_parity("true or false", "true") + assert_parity("false or false", "false") + end + + def test_operator_precedence + assert_parity("false and false or true", "false") + end + + def test_complex_boolean_expressions + assert_parity("true and true and true", "true") + assert_parity("true and false and true", "false") + assert_parity("false or false or true", "true") + end + + def test_boolean_with_variables + assert_parity("a and b", "true", { "a" => true, "b" => true }) + assert_parity("a and b", "false", { "a" => true, "b" => false }) + assert_parity("a or b", "true", { "a" => false, "b" => true }) + assert_parity("a or b", "false", { "a" => false, "b" => false }) + end + + def test_nil_equals_nil + assert_parity("nil == nil", "true") + end + + def test_nil_not_equals_nil + assert_parity("nil != nil", "false") + end + + def test_nil_not_equals_empty_string + assert_parity("nil == ''", "false") + assert_parity("nil != ''", "true") + end + + def test_undefined_variable_in_comparisons + assert_parity("undefined_var == nil", "true") + assert_parity("undefined_var != nil", "false") + end + + def test_undefined_variable_compared_to_empty_string + assert_parity("undefined_var == ''", "false") + assert_parity("undefined_var != ''", "true") + end + + def test_boolean_variable_in_comparisons + assert_parity("t == true", "true", { "t" => true }) + assert_parity("f == false", "true", { "f" => false }) + end + + def test_boolean_variable_compared_to_nil + assert_parity("t == nil", "false", { "t" => true }) + assert_parity("f == nil", "false", { "f" => false }) + assert_parity("f != nil", "true", { "f" => false }) + end + + def test_nil_and_undefined_variables_in_boolean_expressions + assert_parity("x == undefined_var", "true", { "x" => nil }) + assert_parity("x != undefined_var", "false", { "x" => nil }) + end + + def test_nil_literal_in_or_expression + assert_parity("nil or true", "true") + end + + def test_nil_variable_in_or_expression + assert_parity("x or false", "false", { "x" => nil }) + end + + def test_mixed_boolean_expressions + assert_parity("a > b and c < d", "true", { "a" => 99, "b" => 0, "c" => 0, "d" => 99 }) + assert_parity("a > b and c < d", "false", { "a" => 99, "b" => 0, "c" => 99, "d" => 0 }) end def test_boolean_assignment_shorthand template = Liquid::Template.parse("{% assign lazy_load = media_position > 1 %}{{ lazy_load }}") assert_equal("false", template.render("media_position" => 1)) - assert_equal("true", template.render("media_position" => 2)) - end - - def test_boolean_and_operator - template = Liquid::Template.parse("{{ true and true }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ true and false }}") - assert_equal("false", template.render) - end - - def test_boolean_or_operator - template = Liquid::Template.parse("{{ true or false }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ false or false }}") - assert_equal("false", template.render) - end - - def test_operator_precedence_with_parentheses - template = Liquid::Template.parse("{{ false and (false or true) }}") - assert_equal("false", template.render) - end - - def test_operator_precedence_without_parentheses - template = Liquid::Template.parse("{{ false and false or true }}") - assert_equal("true", template.render) - end - - def test_complex_boolean_expressions - template = Liquid::Template.parse("{{ true and true and true }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ true and false and true }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{{ false or false or true }}") - assert_equal("true", template.render) - end - - def test_boolean_with_variables - template = Liquid::Template.parse("{{ a and b }}") - assert_equal("true", template.render("a" => true, "b" => true)) - assert_equal("false", template.render("a" => true, "b" => false)) - - template = Liquid::Template.parse("{{ a or b }}") - assert_equal("true", template.render("a" => false, "b" => true)) - assert_equal("false", template.render("a" => false, "b" => false)) - end - - def test_mixed_boolean_expressions - template = Liquid::Template.parse("{{ a > b and c < d }}") - assert_equal("true", template.render("a" => 5, "b" => 3, "c" => 2, "d" => 4)) - assert_equal("false", template.render("a" => 5, "b" => 3, "c" => 5, "d" => 4)) + assert_equal("true", template.render("media_position" => 2)) end def test_equality_operators - template = Liquid::Template.parse("{{ 1 == 1 }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ 1 != 2 }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ 'hello' == 'hello' }}") - assert_equal("true", template.render) + assert_parity_todo!("1 == 1", "true") + assert_parity_todo!("1 != 2", "true") + assert_parity_todo!("'hello' == 'hello'", "true") end - def test_truthy_falsy_values - template = Liquid::Template.parse("{% if empty_string %}truthy{% else %}falsey{% endif %}") - assert_equal("falsey", template.render("empty_string" => "")) - - template = Liquid::Template.parse("{% if zero %}truthy{% else %}falsey{% endif %}") - assert_equal("falsey", template.render("zero" => 0)) - - template = Liquid::Template.parse("{% if text %}truthy{% else %}falsey{% endif %}") - assert_equal("true", template.render("text" => "hello")) - end - - def test_string_comparison_with_blank - # Non-empty string against blank - template = Liquid::Template.parse("{{ text != blank }}") - assert_equal("true", template.render("text" => "hello")) - - template = Liquid::Template.parse("{{ text == blank }}") - assert_equal("false", template.render("text" => "hello")) - - # Empty string against blank - template = Liquid::Template.parse("{{ empty_text != blank }}") - assert_equal("false", template.render("empty_text" => "")) - - template = Liquid::Template.parse("{{ empty_text == blank }}") - assert_equal("true", template.render("empty_text" => "")) + def test_nil_renders_as_empty_string + assert_parity_todo!("nil", "false") end def test_nil_comparison_with_blank - template = Liquid::Template.parse("{{ nil_value != blank }}") - assert_equal("false", template.render("nil_value" => nil)) - - template = Liquid::Template.parse("{{ nil_value == blank }}") - assert_equal("true", template.render("nil_value" => nil)) - - # Undefined variable is treated as nil - template = Liquid::Template.parse("{{ undefined != blank }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{{ undefined == blank }}") - assert_equal("true", template.render) - end - - def test_empty_collections_with_blank - template = Liquid::Template.parse("{{ empty_array == blank }}") - assert_equal("true", template.render("empty_array" => [])) - - template = Liquid::Template.parse("{{ empty_array != blank }}") - assert_equal("false", template.render("empty_array" => [])) - - template = Liquid::Template.parse("{{ empty_hash == blank }}") - assert_equal("true", template.render("empty_hash" => {})) - - template = Liquid::Template.parse("{{ empty_hash != blank }}") - assert_equal("false", template.render("empty_hash" => {})) - - # Non-empty collections - template = Liquid::Template.parse("{{ array == blank }}") - assert_equal("false", template.render("array" => [1, 2, 3])) - - template = Liquid::Template.parse("{{ hash == blank }}") - assert_equal("false", template.render("hash" => { "key" => "value" })) - end - - def test_blank_in_conditional_statements - template = Liquid::Template.parse("{% if text != blank %}not blank{% else %}is blank{% endif %}") - assert_equal("not blank", template.render("text" => "hello")) - assert_equal("is blank", template.render("text" => "")) - - template = Liquid::Template.parse("{% if nil_value != blank %}not blank{% else %}is blank{% endif %}") - assert_equal("is blank", template.render("nil_value" => nil)) - - template = Liquid::Template.parse("{% if array != blank %}not blank{% else %}is blank{% endif %}") - assert_equal("not blank", template.render("array" => [1, 2, 3])) - assert_equal("is blank", template.render("array" => [])) - end - - def test_blank_with_other_operators - template = Liquid::Template.parse("{{ text != blank and number > 0 }}") - assert_equal("true", template.render("text" => "hello", "number" => 5)) - assert_equal("false", template.render("text" => "", "number" => 5)) - assert_equal("false", template.render("text" => "hello", "number" => 0)) - - template = Liquid::Template.parse("{{ text != blank or number > 0 }}") - assert_equal("true", template.render("text" => "hello", "number" => 0)) - assert_equal("true", template.render("text" => "", "number" => 5)) - assert_equal("false", template.render("text" => "", "number" => 0)) - end - - def test_basic_if_else_conditions - template = Liquid::Template.parse("{% if true %}success{% else %}failure{% endif %}") - assert_equal("success", template.render) - - template = Liquid::Template.parse("{% if false %}failure{% else %}success{% endif %}") - assert_equal("success", template.render) - end - - def test_if_with_comparisons - template = Liquid::Template.parse("{% if 10 > 5 %}greater{% else %}not greater{% endif %}") - assert_equal("greater", template.render) - - template = Liquid::Template.parse("{% if 5 == 5 %}equal{% else %}not equal{% endif %}") - assert_equal("equal", template.render) - - template = Liquid::Template.parse("{% if 3 < 2 %}smaller{% else %}not smaller{% endif %}") - assert_equal("not smaller", template.render) + assert_parity_todo!("nil_value == blank", "false") + assert_parity_todo!("nil_value != blank", "true") + assert_parity_todo!("undefined != blank", "true") + assert_parity_todo!("undefined == blank", "false") end def test_if_with_variables - template = Liquid::Template.parse("{% if value %}has value{% else %}no value{% endif %}") - assert_equal("has value", template.render("value" => true)) - assert_equal("no value", template.render("value" => false)) - assert_equal("no value", template.render("value" => nil)) - assert_equal("has value", template.render("value" => "text")) - assert_equal("no value", template.render("value" => "")) - end - - def test_if_with_variable_comparisons - template = Liquid::Template.parse("{% if count > 5 %}high{% else %}low{% endif %}") - assert_equal("high", template.render("count" => 10)) - assert_equal("low", template.render("count" => 3)) - end - - def test_nested_if_conditions - template = Liquid::Template.parse("{% if a %}{% if b %}both{% else %}a only{% endif %}{% else %}none{% endif %}") - assert_equal("both", template.render("a" => true, "b" => true)) - assert_equal("a only", template.render("a" => true, "b" => false)) - assert_equal("none", template.render("a" => false, "b" => true)) - end - - def test_elsif_conditions - template = Liquid::Template.parse("{% if a %}a{% elsif b %}b{% else %}c{% endif %}") - assert_equal("a", template.render("a" => true, "b" => true)) - assert_equal("b", template.render("a" => false, "b" => true)) - assert_equal("c", template.render("a" => false, "b" => false)) - end - - def test_unless_conditions - template = Liquid::Template.parse("{% unless a %}not a{% else %}a{% endunless %}") - assert_equal("a", template.render("a" => true)) - assert_equal("not a", template.render("a" => false)) - end - - def test_if_with_comparison_and_logical_operator - template = Liquid::Template.parse("{% if a > 5 and b < 10 %}valid{% else %}invalid{% endif %}") - assert_equal("valid", template.render("a" => 7, "b" => 8)) - assert_equal("invalid", template.render("a" => 3, "b" => 8)) - assert_equal("invalid", template.render("a" => 7, "b" => 12)) - end - - # Basic nil rendering tests - def test_nil_renders_as_empty_string - template = Liquid::Template.parse("{{ nil }}") - assert_equal("", template.render) - end - - def test_nil_in_assigned_variable_renders_as_empty_string - template = Liquid::Template.parse("{% assign x = nil %}{{ x }}") - assert_equal("", template.render) - end - - # Nil comparison tests - def test_nil_equals_nil - template = Liquid::Template.parse("{{ nil == nil }}") - assert_equal("true", template.render) - end - - def test_nil_not_equals_nil - template = Liquid::Template.parse("{{ nil != nil }}") - assert_equal("false", template.render) - end - - def test_nil_not_equals_empty_string - template = Liquid::Template.parse("{{ nil == '' }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{{ nil != '' }}") - assert_equal("true", template.render) - end - - # Variable tests with nil values - def test_variable_with_nil_value_in_comparisons - template = Liquid::Template.parse("{% assign x = nil %}{{ x == nil }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{% assign x = nil %}{{ x != nil }}") - assert_equal("false", template.render) - end - - def test_variable_with_nil_value_compared_to_empty_string - template = Liquid::Template.parse("{% assign x = nil %}{{ x == '' }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{% assign x = nil %}{{ x != '' }}") - assert_equal("true", template.render) - end - - # Tests with undefined variables - def test_undefined_variable_in_comparisons - template = Liquid::Template.parse("{{ undefined_var == nil }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{{ undefined_var != nil }}") - assert_equal("false", template.render) - end - - def test_undefined_variable_compared_to_empty_string - template = Liquid::Template.parse("{{ undefined_var == '' }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{{ undefined_var != '' }}") - assert_equal("true", template.render) - end - - # Tests with boolean values - def test_boolean_variable_in_comparisons - template = Liquid::Template.parse("{% assign t = true %}{{ t == true }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{% assign f = false %}{{ f == false }}") - assert_equal("true", template.render) - end - - def test_boolean_variable_compared_to_nil - template = Liquid::Template.parse("{% assign t = true %}{{ t == nil }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{% assign f = false %}{{ f == nil }}") - assert_equal("false", template.render) - - template = Liquid::Template.parse("{% assign f = false %}{{ f != nil }}") - assert_equal("true", template.render) - end - - # Mixed comparison tests - def test_nil_and_undefined_variables_in_boolean_expressions - template = Liquid::Template.parse("{% assign x = nil %}{{ x == undefined_var }}") - assert_equal("true", template.render) - - template = Liquid::Template.parse("{% assign x = nil %}{{ x != undefined_var }}") - assert_equal("false", template.render) + assert_parity_todo!("value", "true", { "value" => true }) + assert_parity_todo!("value", "false", { "value" => false }) + assert_parity_todo!("value", "false", { "value" => nil }) + assert_parity_todo!("value", "true", { "value" => "text" }) + assert_parity_todo!("value", "true", { "value" => "" }) end def test_nil_variable_in_and_expression - template = Liquid::Template.parse("{% assign x = nil %}{{ x and true }}") - assert_equal("false", template.render) + assert_parity_todo!("x and true", "false", { "x" => nil }) end - def test_nil_literal_in_or_expression - template = Liquid::Template.parse("{% assign x = nil %}{{ nil or true }}") - assert_equal("true", template.render) + private + + def assert_parity_todo!(liquid_expression, expected_result, args = {}) + assert_parity_scenario(:condition, "{% if #{liquid_expression} %}true{% else %}false{% endif %}", expected_result, args) + test_name = caller_locations(1, 1)[0].label + puts "\e[33mTODO: parity for '#{test_name}'\e[0m" end - def test_nil_variable_in_or_expression - template = Liquid::Template.parse("{% assign x = nil %}{{ x or false }}") - assert_equal("false", template.render) + def assert_parity(liquid_expression, expected_result, args = {}) + assert_parity_scenario(:condition, "{% if #{liquid_expression} %}true{% else %}false{% endif %}", expected_result, args) + assert_parity_scenario(:expression, "{{ #{liquid_expression} }}", expected_result, args) + end + + def assert_parity_scenario(kind, template, exp_output, args = {}) + act_output = Liquid::Template.parse(template).render(args) + + assert_equal(exp_output, act_output, <<~ERROR_MESSAGE) + #{kind.to_s.capitalize} template failure: + --- + #{template} + --- + args: #{args.inspect} + ERROR_MESSAGE end end