mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-26 13:45:13 -07:00
Merge pull request #2034 from Shopify/fix-liquid-spec-without-activesupport
Require liquid-spec to be run on commit automatically and related fixes
This commit is contained in:
@@ -9,26 +9,29 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
entry:
|
entry:
|
||||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
- { ruby: 3.3, allowed-failure: false } # minimum supported
|
||||||
- { ruby: 3.2, allowed-failure: false }
|
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
||||||
- { ruby: 3.3, allowed-failure: false }
|
- { ruby: 4.0, allowed-failure: false } # latest stable
|
||||||
- { ruby: 3.3, allowed-failure: false }
|
|
||||||
- { ruby: 3.4, allowed-failure: false } # latest
|
|
||||||
- {
|
- {
|
||||||
ruby: 3.4,
|
ruby: 4.0,
|
||||||
allowed-failure: false,
|
allowed-failure: false,
|
||||||
rubyopt: "--enable-frozen-string-literal",
|
rubyopt: "--enable-frozen-string-literal",
|
||||||
}
|
}
|
||||||
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
|
- { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" }
|
||||||
- { ruby: head, allowed-failure: false }
|
- { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" }
|
||||||
|
|
||||||
|
# Head can have failures due to being in development
|
||||||
|
- { ruby: head, allowed-failure: true }
|
||||||
- {
|
- {
|
||||||
ruby: head,
|
ruby: head,
|
||||||
allowed-failure: false,
|
allowed-failure: true,
|
||||||
rubyopt: "--enable-frozen-string-literal",
|
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 }}
|
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
@@ -42,6 +45,23 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
RUBYOPT: ${{ matrix.entry.rubyopt }}
|
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:
|
memory_profile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+10
-1
@@ -174,7 +174,16 @@ Style/WordArray:
|
|||||||
|
|
||||||
# Offense count: 117
|
# Offense count: 117
|
||||||
# This cop supports safe auto-correction (--auto-correct).
|
# 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
|
# URISchemes: http, https
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 260
|
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'
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ group :development do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rubocop', '~> 1.61.0'
|
gem 'benchmark'
|
||||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
gem 'rubocop', '~> 1.82.0'
|
||||||
|
gem 'rubocop-shopify', '~> 2.18.0', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
end
|
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
|
||||||
|
|||||||
+51
-12
@@ -113,24 +113,63 @@ module Liquid
|
|||||||
|
|
||||||
def equal_variables(left, right)
|
def equal_variables(left, right)
|
||||||
if left.is_a?(MethodLiteral)
|
if left.is_a?(MethodLiteral)
|
||||||
if right.respond_to?(left.method_name)
|
return call_method_literal(left, right)
|
||||||
return right.send(left.method_name)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if right.is_a?(MethodLiteral)
|
if right.is_a?(MethodLiteral)
|
||||||
if left.respond_to?(right.method_name)
|
return call_method_literal(right, left)
|
||||||
return left.send(right.method_name)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
left == right
|
left == right
|
||||||
end
|
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)
|
def interpret_condition(left, right, op, context)
|
||||||
# If the operator is empty this means that the decision statement is just
|
# 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
|
# a single variable. We can just poll this variable from the context and
|
||||||
@@ -154,8 +193,8 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def deprecated_default_context
|
def deprecated_default_context
|
||||||
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
||||||
" and will be removed from Liquid 6.0.0.")
|
"and will be removed from Liquid 6.0.0.")
|
||||||
Context.new
|
Context.new
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ module Liquid
|
|||||||
|
|
||||||
# Catch all for the method
|
# Catch all for the method
|
||||||
def liquid_method_missing(method)
|
def liquid_method_missing(method)
|
||||||
return nil unless @context&.strict_variables
|
return unless @context&.strict_variables
|
||||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def inner_parse(markup, ss, cache)
|
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(
|
return RangeLookup.parse(
|
||||||
Regexp.last_match(1),
|
Regexp.last_match(1),
|
||||||
Regexp.last_match(2),
|
Regexp.last_match(2),
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ module Liquid
|
|||||||
def interpolate(name, vars)
|
def interpolate(name, vars)
|
||||||
name.gsub(/%\{(\w+)\}/) do
|
name.gsub(/%\{(\w+)\}/) do
|
||||||
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
# 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -768,6 +768,8 @@ module Liquid
|
|||||||
# @liquid_syntax array | first
|
# @liquid_syntax array | first
|
||||||
# @liquid_return [untyped]
|
# @liquid_return [untyped]
|
||||||
def first(array)
|
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)
|
array.first if array.respond_to?(:first)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -779,6 +781,8 @@ module Liquid
|
|||||||
# @liquid_syntax array | last
|
# @liquid_syntax array | last
|
||||||
# @liquid_return [untyped]
|
# @liquid_return [untyped]
|
||||||
def last(array)
|
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)
|
array.last if array.respond_to?(:last)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ module Liquid
|
|||||||
byte_a = byte_b = @ss.scan_byte
|
byte_a = byte_b = @ss.scan_byte
|
||||||
|
|
||||||
while byte_b
|
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
|
break unless byte_a
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ module Liquid
|
|||||||
return obj if obj.respond_to?(:strftime)
|
return obj if obj.respond_to?(:strftime)
|
||||||
|
|
||||||
if obj.is_a?(String)
|
if obj.is_a?(String)
|
||||||
return nil if obj.empty?
|
return if obj.empty?
|
||||||
obj = obj.downcase
|
obj = obj.downcase
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ module Liquid
|
|||||||
elsif lookup_command?(i) && object.respond_to?(key)
|
elsif lookup_command?(i) && object.respond_to?(key)
|
||||||
object = object.send(key).to_liquid
|
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
|
# 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
|
# keywords either. The only thing we got left is to return nil or
|
||||||
# raise an exception if `strict_variables` option is set to true
|
# raise an exception if `strict_variables` option is set to true
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -59,7 +59,7 @@ class SecurityTest < Minitest::Test
|
|||||||
|
|
||||||
GC.start
|
GC.start
|
||||||
|
|
||||||
assert_equal([], (Symbol.all_symbols - current_symbols))
|
assert_equal([], Symbol.all_symbols - current_symbols)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_does_not_add_drop_methods_to_symbol_table
|
def test_does_not_add_drop_methods_to_symbol_table
|
||||||
@@ -70,7 +70,7 @@ class SecurityTest < Minitest::Test
|
|||||||
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
|
||||||
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", 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
|
end
|
||||||
|
|
||||||
def test_max_depth_nested_blocks_does_not_raise_exception
|
def test_max_depth_nested_blocks_does_not_raise_exception
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_slice_on_arrays
|
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), @filters.slice(input, 1, 3))
|
||||||
assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
|
assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
|
||||||
assert_equal(%w(), @filters.slice(input, 1, 0))
|
assert_equal(%w(), @filters.slice(input, 1, 0))
|
||||||
@@ -627,6 +627,40 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_nil(@filters.last([]))
|
assert_nil(@filters.last([]))
|
||||||
end
|
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
|
def test_replace
|
||||||
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
|
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))
|
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
|
||||||
@@ -1296,7 +1330,7 @@ class StandardFiltersTest < Minitest::Test
|
|||||||
assert_equal(1, @filters.sum(input, true))
|
assert_equal(1, @filters.sum(input, true))
|
||||||
assert_equal(0.2, @filters.sum(input, 1.0))
|
assert_equal(0.2, @filters.sum(input, 1.0))
|
||||||
assert_equal(-0.3, @filters.sum(input, 1))
|
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, nil))
|
||||||
assert_equal(0, @filters.sum(input, ""))
|
assert_equal(0, @filters.sum(input, ""))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class StandardTagTest < Minitest::Test
|
|||||||
assigns = { 'condition' => "bad string here" }
|
assigns = { 'condition' => "bad string here" }
|
||||||
assert_template_result(
|
assert_template_result(
|
||||||
'',
|
'',
|
||||||
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
|
'{% case condition %}{% when "string here" %} hit {% endcase %}',
|
||||||
assigns,
|
assigns,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class TemplateTest < Minitest::Test
|
|||||||
assert(t.resource_limits.reached?)
|
assert(t.resource_limits.reached?)
|
||||||
|
|
||||||
t.resource_limits.render_score_limit = 200
|
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)
|
refute_nil(t.resource_limits.render_score)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
assert_equal(true, Condition.new(1, '==', 1).evaluate)
|
||||||
end
|
end
|
||||||
|
|
||||||
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
|
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \
|
||||||
" and will be removed from Liquid 6.0.0."
|
"and will be removed from Liquid 6.0.0."
|
||||||
assert_includes(err.lines.map(&:strip), expected)
|
assert_includes(err.lines.map(&:strip), expected)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -197,6 +197,172 @@ class ConditionUnitTest < Minitest::Test
|
|||||||
assert_equal(['title'], result.lookups)
|
assert_equal(['title'], result.lookups)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def assert_evaluates_true(left, op, right)
|
def assert_evaluates_true(left, op, right)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test
|
|||||||
def test_add_filter_when_wrong_filter_class
|
def test_add_filter_when_wrong_filter_class
|
||||||
c = Context.new
|
c = Context.new
|
||||||
s = c.strainer
|
s = c.strainer
|
||||||
wrong_filter = ->(v) { v.reverse }
|
wrong_filter = lambda(&:reverse)
|
||||||
|
|
||||||
exception = assert_raises(TypeError) do
|
exception = assert_raises(TypeError) do
|
||||||
s.class.add_filter(wrong_filter)
|
s.class.add_filter(wrong_filter)
|
||||||
|
|||||||
Reference in New Issue
Block a user