diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index bd0df8e5b..de57dd68d 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -9,26 +9,29 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: entry: - - { ruby: 3.0, allowed-failure: false } # minimum supported - - { ruby: 3.2, allowed-failure: false } - - { ruby: 3.3, allowed-failure: false } - - { ruby: 3.3, allowed-failure: false } - - { ruby: 3.4, allowed-failure: false } # latest + - { ruby: 3.3, allowed-failure: false } # minimum supported + - { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: 4.0, allowed-failure: false } # latest stable - { - ruby: 3.4, + ruby: 4.0, allowed-failure: false, rubyopt: "--enable-frozen-string-literal", } - - { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" } - - { ruby: head, allowed-failure: false } + - { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" } + + # Head can have failures due to being in development + - { ruby: head, allowed-failure: true } - { ruby: head, - allowed-failure: false, + allowed-failure: true, rubyopt: "--enable-frozen-string-literal", } - - { ruby: head, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: head, allowed-failure: true, rubyopt: "--yjit" } + - { ruby: head, allowed-failure: true, rubyopt: "--zjit" } name: Test Ruby ${{ matrix.entry.ruby }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 @@ -42,6 +45,23 @@ jobs: env: RUBYOPT: ${{ matrix.entry.rubyopt }} + spec: + runs-on: ubuntu-latest + env: + BUNDLE_WITH: spec + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0 + with: + bundler-cache: true + bundler: latest + - name: Run liquid-spec for all adapters + run: | + for adapter in spec/*.rb; do + echo "=== Running $adapter ===" + bundle exec liquid-spec run "$adapter" --no-max-failures + done + memory_profile: runs-on: ubuntu-latest steps: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 424b9b4d3..1fcb7b54c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -174,7 +174,16 @@ Style/WordArray: # Offense count: 117 # This cop supports safe auto-correction (--auto-correct). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: Max: 260 + +Naming/PredicatePrefix: + Enabled: false + +# Offense count: 1 +# This is intentional - early return from begin/rescue in assignment context +Lint/NoReturnInBeginEndBlocks: + Exclude: + - 'lib/liquid/standardfilters.rb' diff --git a/Gemfile b/Gemfile index a404c0d4b..add479637 100644 --- a/Gemfile +++ b/Gemfile @@ -25,7 +25,14 @@ group :development do end group :test do - gem 'rubocop', '~> 1.61.0' - gem 'rubocop-shopify', '~> 2.12.0', require: false + gem 'benchmark' + gem 'rubocop', '~> 1.82.0' + gem 'rubocop-shopify', '~> 2.18.0', require: false gem 'rubocop-performance', require: false end + +group :spec do + # Using feature branch until https://github.com/Shopify/liquid-spec/pull/97 is merged + gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'add-per-spec-required-features' + gem 'activesupport', require: false +end diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 9ab350f07..eb674ced8 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -113,24 +113,63 @@ def inspect def equal_variables(left, right) if left.is_a?(MethodLiteral) - if right.respond_to?(left.method_name) - return right.send(left.method_name) - else - return nil - end + return call_method_literal(left, right) end if right.is_a?(MethodLiteral) - if left.respond_to?(right.method_name) - return left.send(right.method_name) - else - return nil - end + return call_method_literal(right, left) end left == right end + def call_method_literal(literal, value) + method_name = literal.method_name + + # If the object responds to the method, use it + if value.respond_to?(method_name) + return value.send(method_name) + end + + # Implement blank?/empty? for common types that don't have it + # (ActiveSupport adds these, but Liquid should work without it) + case method_name + when :blank? + liquid_blank?(value) + when :empty? + liquid_empty?(value) + end + end + + # Implement blank? semantics matching ActiveSupport + def liquid_blank?(value) + case value + when NilClass, FalseClass + true + when TrueClass, Numeric + false + when String + # Blank if empty or whitespace only + value.empty? || value.match?(/\A\s*\z/) + when Array, Hash + value.empty? + else + # Fall back to empty? if available, otherwise false + value.respond_to?(:empty?) ? value.empty? : false + end + end + + # Implement empty? semantics + # Note: nil is NOT empty (but IS blank). empty? checks if a collection has zero elements. + def liquid_empty?(value) + case value + when String, Array, Hash + value.empty? + else + value.respond_to?(:empty?) ? value.empty? : false + end + end + def interpret_condition(left, right, op, context) # If the operator is empty this means that the decision statement is just # a single variable. We can just poll this variable from the context and @@ -154,8 +193,8 @@ def interpret_condition(left, right, op, context) end def deprecated_default_context - warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \ - " and will be removed from Liquid 6.0.0.") + warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \ + "and will be removed from Liquid 6.0.0.") Context.new end diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index b990630e4..d13398ec4 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -31,7 +31,7 @@ def initialize # Catch all for the method def liquid_method_missing(method) - return nil unless @context&.strict_variables + return unless @context&.strict_variables raise Liquid::UndefinedDropMethod, "undefined method #{method}" end diff --git a/lib/liquid/expression.rb b/lib/liquid/expression.rb index 2605c5576..00c40a4c3 100644 --- a/lib/liquid/expression.rb +++ b/lib/liquid/expression.rb @@ -55,7 +55,7 @@ def parse(markup, ss = StringScanner.new(""), cache = nil) end def inner_parse(markup, ss, cache) - if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX + if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX return RangeLookup.parse( Regexp.last_match(1), Regexp.last_match(2), diff --git a/lib/liquid/i18n.rb b/lib/liquid/i18n.rb index 4a2885e86..982688115 100644 --- a/lib/liquid/i18n.rb +++ b/lib/liquid/i18n.rb @@ -28,7 +28,7 @@ def locale def interpolate(name, vars) name.gsub(/%\{(\w+)\}/) do # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] - (vars[Regexp.last_match(1).to_sym]).to_s + vars[Regexp.last_match(1).to_sym].to_s end end diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 6e072fcf5..24e135d91 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -768,6 +768,8 @@ def date(input, format) # @liquid_syntax array | first # @liquid_return [untyped] def first(array) + # ActiveSupport returns "" for empty strings, not nil + return array[0] || "" if array.is_a?(String) array.first if array.respond_to?(:first) end @@ -779,6 +781,8 @@ def first(array) # @liquid_syntax array | last # @liquid_return [untyped] def last(array) + # ActiveSupport returns "" for empty strings, not nil + return array[-1] || "" if array.is_a?(String) array.last if array.respond_to?(:last) end diff --git a/lib/liquid/tokenizer.rb b/lib/liquid/tokenizer.rb index 56dd218ec..8b331d93c 100644 --- a/lib/liquid/tokenizer.rb +++ b/lib/liquid/tokenizer.rb @@ -117,7 +117,7 @@ def next_variable_token byte_a = byte_b = @ss.scan_byte while byte_b - byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY) + byte_a = @ss.scan_byte while byte_a && byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY break unless byte_a diff --git a/lib/liquid/utils.rb b/lib/liquid/utils.rb index eb0a02a9f..084739a21 100644 --- a/lib/liquid/utils.rb +++ b/lib/liquid/utils.rb @@ -69,7 +69,7 @@ def self.to_date(obj) return obj if obj.respond_to?(:strftime) if obj.is_a?(String) - return nil if obj.empty? + return if obj.empty? obj = obj.downcase end diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb index 340c0b66d..4fba2a658 100644 --- a/lib/liquid/variable_lookup.rb +++ b/lib/liquid/variable_lookup.rb @@ -70,6 +70,11 @@ def evaluate(context) elsif lookup_command?(i) && object.respond_to?(key) object = object.send(key).to_liquid + # Handle string first/last like ActiveSupport does (returns first/last character) + # ActiveSupport returns "" for empty strings, not nil + elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last") + object = key == "first" ? (object[0] || "") : (object[-1] || "") + # No key was present with the desired value and it wasn't one of the directly supported # keywords either. The only thing we got left is to return nil or # raise an exception if `strict_variables` option is set to true diff --git a/spec/ruby_liquid.rb b/spec/ruby_liquid.rb new file mode 100644 index 000000000..5eda72da4 --- /dev/null +++ b/spec/ruby_liquid.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation) +# +# Run with: bundle exec liquid-spec run spec/ruby_liquid.rb + +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'liquid' + +LiquidSpec.configure do |config| + # Run core Liquid specs + config.features = [:core] +end + +# Compile a template string into a Liquid::Template +LiquidSpec.compile do |source, options| + Liquid::Template.parse(source, **options) +end + +# Render a compiled template with the given context +# @param template [Liquid::Template] compiled template +# @param assigns [Hash] environment variables +# @param options [Hash] :registers, :strict_errors, :exception_renderer +LiquidSpec.render do |template, assigns, options| + registers = Liquid::Registers.new(options[:registers] || {}) + + context = Liquid::Context.build( + static_environments: assigns, + registers: registers, + rethrow_errors: options[:strict_errors], + ) + + context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] + + template.render(context) +end diff --git a/spec/ruby_liquid_with_active_support.rb b/spec/ruby_liquid_with_active_support.rb new file mode 100644 index 000000000..4a6f00570 --- /dev/null +++ b/spec/ruby_liquid_with_active_support.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Liquid Spec Adapter for Shopify/liquid with ActiveSupport loaded +# +# Run with: bundle exec liquid-spec run spec/ruby_liquid_with_active_support.rb + +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'active_support/all' +require 'liquid' + +LiquidSpec.configure do |config| + # Run core Liquid specs plus ActiveSupport SafeBuffer tests + config.features = [:core, :activesupport] +end + +# Compile a template string into a Liquid::Template +LiquidSpec.compile do |source, options| + Liquid::Template.parse(source, **options) +end + +# Render a compiled template with the given context +# @param template [Liquid::Template] compiled template +# @param assigns [Hash] environment variables +# @param options [Hash] :registers, :strict_errors, :exception_renderer +LiquidSpec.render do |template, assigns, options| + registers = Liquid::Registers.new(options[:registers] || {}) + + context = Liquid::Context.build( + static_environments: assigns, + registers: registers, + rethrow_errors: options[:strict_errors], + ) + + context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] + + template.render(context) +end diff --git a/test/integration/security_test.rb b/test/integration/security_test.rb index 75f092965..f84058940 100644 --- a/test/integration/security_test.rb +++ b/test/integration/security_test.rb @@ -59,7 +59,7 @@ def test_does_not_permanently_add_filters_to_symbol_table GC.start - assert_equal([], (Symbol.all_symbols - current_symbols)) + assert_equal([], Symbol.all_symbols - current_symbols) end def test_does_not_add_drop_methods_to_symbol_table @@ -70,7 +70,7 @@ def test_does_not_add_drop_methods_to_symbol_table assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) - assert_equal([], (Symbol.all_symbols - current_symbols)) + assert_equal([], Symbol.all_symbols - current_symbols) end def test_max_depth_nested_blocks_does_not_raise_exception diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index e27605ff5..ef5f15493 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -116,7 +116,7 @@ def test_slice end def test_slice_on_arrays - input = 'foobar'.split(//) + input = 'foobar'.split('') assert_equal(%w(o o b), @filters.slice(input, 1, 3)) assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000)) assert_equal(%w(), @filters.slice(input, 1, 0)) @@ -627,6 +627,40 @@ def test_first_last assert_nil(@filters.last([])) end + def test_first_last_on_strings + # Ruby's String class does not have first/last methods by default. + # ActiveSupport adds String#first and String#last to return the first/last character. + # Liquid must work without ActiveSupport, so the first/last filters handle strings specially. + # + # This enables template patterns like: + # {{ product.title | first }} => "S" (for "Snowboard") + # {{ customer.name | last }} => "h" (for "Smith") + # + # Note: ActiveSupport returns "" for empty strings, not nil. + assert_equal('f', @filters.first('foo')) + assert_equal('o', @filters.last('foo')) + assert_equal('', @filters.first('')) + assert_equal('', @filters.last('')) + end + + def test_first_last_on_unicode_strings + # Unicode strings should return the first/last grapheme cluster (character), + # not the first/last byte. Ruby's String#[] handles this correctly with index 0/-1. + # This ensures international text works properly: + # {{ korean_name | first }} => "고" (not a partial byte sequence) + assert_equal('고', @filters.first('고스트빈')) + assert_equal('빈', @filters.last('고스트빈')) + end + + def test_first_last_on_strings_via_template + # Integration test to verify the filter works end-to-end in templates. + # Empty strings return empty output (nil renders as empty string). + assert_template_result('f', '{{ name | first }}', { 'name' => 'foo' }) + assert_template_result('o', '{{ name | last }}', { 'name' => 'foo' }) + assert_template_result('', '{{ name | first }}', { 'name' => '' }) + assert_template_result('', '{{ name | last }}', { 'name' => '' }) + end + def test_replace assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b')) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2)) @@ -1296,7 +1330,7 @@ def test_sum_with_non_string_property assert_equal(1, @filters.sum(input, true)) assert_equal(0.2, @filters.sum(input, 1.0)) assert_equal(-0.3, @filters.sum(input, 1)) - assert_equal(0.4, @filters.sum(input, (1..5))) + assert_equal(0.4, @filters.sum(input, 1..5)) assert_equal(0, @filters.sum(input, nil)) assert_equal(0, @filters.sum(input, "")) end diff --git a/test/integration/tags/standard_tag_test.rb b/test/integration/tags/standard_tag_test.rb index 6c297d407..67471e735 100644 --- a/test/integration/tags/standard_tag_test.rb +++ b/test/integration/tags/standard_tag_test.rb @@ -117,7 +117,7 @@ def test_case assigns = { 'condition' => "bad string here" } assert_template_result( '', - '{% case condition %}{% when "string here" %} hit {% endcase %}',\ + '{% case condition %}{% when "string here" %} hit {% endcase %}', assigns, ) end diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb index 92ece017c..c01c7161c 100644 --- a/test/integration/template_test.rb +++ b/test/integration/template_test.rb @@ -133,7 +133,7 @@ def test_resource_limits_render_score assert(t.resource_limits.reached?) t.resource_limits.render_score_limit = 200 - assert_equal((" foo " * 100), t.render!) + assert_equal(" foo " * 100, t.render!) refute_nil(t.resource_limits.render_score) end diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb index eb466f7a9..d7f35f609 100644 --- a/test/unit/condition_unit_test.rb +++ b/test/unit/condition_unit_test.rb @@ -161,8 +161,8 @@ def test_default_context_is_deprecated assert_equal(true, Condition.new(1, '==', 1).evaluate) end - expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \ - " and will be removed from Liquid 6.0.0." + expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \ + "and will be removed from Liquid 6.0.0." assert_includes(err.lines.map(&:strip), expected) end @@ -197,6 +197,172 @@ def test_parse_expression_with_safe_true_in_strict2_mode assert_equal(['title'], result.lookups) end + # Tests for blank? comparison without ActiveSupport + # + # Ruby's standard library does not include blank? on String, Array, Hash, etc. + # ActiveSupport adds blank? but Liquid must work without it. These tests verify + # that Liquid implements blank? semantics internally for use in templates like: + # {% if x == blank %}...{% endif %} + # + # The blank? semantics match ActiveSupport's behavior: + # - nil and false are blank + # - Strings are blank if empty or contain only whitespace + # - Arrays and Hashes are blank if empty + # - true and numbers are never blank + + def test_blank_with_whitespace_string + # Template authors expect " " to be blank since it has no visible content. + # This matches ActiveSupport's String#blank? which returns true for whitespace-only strings. + @context['whitespace'] = ' ' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('whitespace'), '==', blank_literal) + end + + def test_blank_with_empty_string + # An empty string has no content, so it should be considered blank. + # This is the most basic case of a blank string. + @context['empty_string'] = '' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_string'), '==', blank_literal) + end + + def test_blank_with_empty_array + # Empty arrays have no elements, so they are blank. + # Useful for checking if a collection has items: {% if products == blank %} + @context['empty_array'] = [] + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_array'), '==', blank_literal) + end + + def test_blank_with_empty_hash + # Empty hashes have no key-value pairs, so they are blank. + # Useful for checking if settings/options exist: {% if settings == blank %} + @context['empty_hash'] = {} + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_hash'), '==', blank_literal) + end + + def test_blank_with_nil + # nil represents "nothing" and is the canonical blank value. + # Unassigned variables resolve to nil, so this enables: {% if missing_var == blank %} + @context['nil_value'] = nil + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('nil_value'), '==', blank_literal) + end + + def test_blank_with_false + # false is considered blank to match ActiveSupport semantics. + # This allows {% if some_flag == blank %} to work when flag is false. + @context['false_value'] = false + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('false_value'), '==', blank_literal) + end + + def test_not_blank_with_true + # true is a definite value, not blank. + # Ensures {% if flag == blank %} works correctly for boolean flags. + @context['true_value'] = true + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('true_value'), '==', blank_literal) + end + + def test_not_blank_with_number + # Numbers (including zero) are never blank - they represent actual values. + # 0 is a valid quantity, not the absence of a value. + @context['number'] = 42 + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('number'), '==', blank_literal) + end + + def test_not_blank_with_string_content + # A string with actual content is not blank. + # This is the expected behavior for most template string comparisons. + @context['string'] = 'hello' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('string'), '==', blank_literal) + end + + def test_not_blank_with_non_empty_array + # An array with elements has content, so it's not blank. + # Enables patterns like {% unless products == blank %}Show products{% endunless %} + @context['array'] = [1, 2, 3] + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('array'), '==', blank_literal) + end + + def test_not_blank_with_non_empty_hash + # A hash with key-value pairs has content, so it's not blank. + # Useful for checking if configuration exists: {% if config != blank %} + @context['hash'] = { 'a' => 1 } + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('hash'), '==', blank_literal) + end + + # Tests for empty? comparison without ActiveSupport + # + # empty? is distinct from blank? - it only checks if a collection has zero elements. + # For strings, empty? checks length == 0, NOT whitespace content. + # Ruby's standard library has empty? on String, Array, and Hash, but Liquid + # provides a fallback implementation for consistency. + + def test_empty_with_empty_string + # An empty string ("") has length 0, so it's empty. + # Different from blank - empty is a stricter check. + @context['empty_string'] = '' + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_string'), '==', empty_literal) + end + + def test_empty_with_whitespace_string_not_empty + # Whitespace strings have length > 0, so they are NOT empty. + # This is the key difference between empty and blank: + # " ".empty? => false, but " ".blank? => true + @context['whitespace'] = ' ' + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_false(VariableLookup.new('whitespace'), '==', empty_literal) + end + + def test_empty_with_empty_array + # An array with no elements is empty. + # [].empty? => true + @context['empty_array'] = [] + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_array'), '==', empty_literal) + end + + def test_empty_with_empty_hash + # A hash with no key-value pairs is empty. + # {}.empty? => true + @context['empty_hash'] = {} + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal) + end + + def test_nil_is_not_empty + # nil is NOT empty - empty? checks if a collection has zero elements. + # nil is not a collection, so it cannot be empty. + # This differs from blank: nil IS blank, but nil is NOT empty. + @context['nil_value'] = nil + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_false(VariableLookup.new('nil_value'), '==', empty_literal) + end + private def assert_evaluates_true(left, op, right) diff --git a/test/unit/strainer_template_unit_test.rb b/test/unit/strainer_template_unit_test.rb index a04107084..aa3f153a3 100644 --- a/test/unit/strainer_template_unit_test.rb +++ b/test/unit/strainer_template_unit_test.rb @@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test def test_add_filter_when_wrong_filter_class c = Context.new s = c.strainer - wrong_filter = ->(v) { v.reverse } + wrong_filter = lambda(&:reverse) exception = assert_raises(TypeError) do s.class.add_filter(wrong_filter)