From de603f07aff2c706b9cb944cc18f279d8599c9b3 Mon Sep 17 00:00:00 2001 From: Burke Libbey Date: Mon, 10 Jun 2024 13:29:42 -0400 Subject: [PATCH] Import Array and Hash tests, and make `== blank` match behaviour with AS When ActiveSupport isn't loaded, `[] == blank` is false, but when it is loaded, it (correctly?) matches the behaviour of `[] == empty`. I think it's fair to say the intended behaviour is for == blank to match the behaviour of when AS is loaded, so let's just special-case this. We also import a bunch of Array and Hash tests here. --- lib/liquid/condition.rb | 15 +++ test/integration/array_test.rb | 103 +++++++++++++++ test/integration/hash_test.rb | 223 +++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 test/integration/array_test.rb create mode 100644 test/integration/hash_test.rb diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index d35ba301..1c45618e 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -112,6 +112,8 @@ module Liquid if left.is_a?(MethodLiteral) if right.respond_to?(left.method_name) return right.send(left.method_name) + elsif (res = fallback_simulation_of_active_support(left.method_name, right)) + return res else return nil end @@ -120,6 +122,8 @@ module Liquid if right.is_a?(MethodLiteral) if left.respond_to?(right.method_name) return left.send(right.method_name) + elsif (res = fallback_simulation_of_active_support(right.method_name, left)) + return res else return nil end @@ -128,6 +132,17 @@ module Liquid left == right end + # ActiveSupport creates #blank? as an alias for #empty? on Hash and Array. + # Without this simulation, [] == blank behaves differently when AS is loaded vs. not. + def fallback_simulation_of_active_support(method_name, obj) + return nil unless method_name == :blank? + + case obj + when Array, Hash + obj.empty? + end # else nil + 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 diff --git a/test/integration/array_test.rb b/test/integration/array_test.rb new file mode 100644 index 00000000..4aefab09 --- /dev/null +++ b/test/integration/array_test.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ArrayTest < Minitest::Test + include Liquid + + def test_array_size + assert_template_result("0", "{{a.size}}", { 'a' => [] }) + assert_template_result("2", "{{a.size}}", { 'a' => [1, 2] }) + end + + def test_array_first + assert_template_result("", "{{a.first}}", { 'a' => [] }) + assert_template_result("1", "{{a.first}}", { 'a' => [1, 2] }) + end + + def test_array_last + assert_template_result("", "{{a.last}}", { 'a' => [] }) + assert_template_result("2", "{{a.last}}", { 'a' => [1, 2] }) + end + + def test_array_index + assert_template_result("", "{{a[1]}}", { 'a' => [] }) + assert_template_result("2", "{{a[1]}}", { 'a' => [1, 2] }) + end + + def test_negative_array_index + assert_template_result("3", "{{a[-1]}}", { 'a' => [1, 2, 3] }) + assert_template_result("1", "{{a[-3]}}", { 'a' => [1, 2, 3] }) + assert_template_result("", "{{a[-4]}}", { 'a' => [1, 2, 3] }) + end + + def test_array_to_s + arr = ["a", ["b", 1], "c"] + assert_template_result("ab1c", "{{a}}", { 'a' => arr }) + end + + def test_auto_methods + a1 = [] + a2 = [1, 2, 3, "abc"] + assert_template_result("0,nonblank", "{{a.size}},{%if a['size'] == blank%}blank{%else%}nonblank{%endif%}", { 'a' => a1 }) + assert_template_result("4,nonblank", "{{a.size}},{%if a['size'] == blank%}blank{%else%}nonblank{%endif%}", { 'a' => a2 }) + assert_template_result(",", "{{a.first}},{{a['first']}}", { 'a' => a1 }) + assert_template_result("1,", "{{a.first}},{{a['first']}}", { 'a' => a2 }) + assert_template_result(",", "{{a.last}},{{a['last']}}", { 'a' => a1 }) + assert_template_result("abc,", "{{a.last}},{{a['last']}}", { 'a' => a2 }) + end + + def test_array_equality + a0 = [] + a1 = [1] + a2 = [1, 2] + a3 = [1] + + arrays = [a0, a1, a2, a3] + arrays.each_with_index do |a, i| + arrays.each_with_index do |b, j| + prefix = "(#{i},#{j})" + assert_template_result( + prefix + (a == b ? "y" : "n"), + "#{prefix}{% if a == b %}y{%else%}n{%endif%}", + { "a" => a, "b" => b }, + ) + end + end + end + + def test_contains + a0 = [1, "a", ["b", "c"], true] + a1 = ["b", "c"] + a2 = ["B", "C"] + + tpl = "{%if a contains b%}y{%else%}n{%endif%}" + + assert_template_result("y", tpl, { "a" => a0, "b" => 1 }) + assert_template_result("n", tpl, { "a" => a0, "b" => 2 }) + assert_template_result("y", tpl, { "a" => a0, "b" => a1 }) + assert_template_result("n", tpl, { "a" => a0, "b" => a2 }) + end + + def test_empty + assert_template_result("y", "{%if a == empty%}y{%else%}n{%endif%}", { 'a' => [] }) + assert_template_result("n", "{%if a == empty%}y{%else%}n{%endif%}", { 'a' => [1, 2] }) + end + + def test_blank + assert_template_result("y", "{%if a == blank%}y{%else%}n{%endif%}", { 'a' => [] }) + assert_template_result("n", "{%if a == blank%}y{%else%}n{%endif%}", { 'a' => [1, 2] }) + end + + def test_iter + a0 = [1, "a"] + assert_template_result("1a", "{%for i in a%}{{i}}{%endfor%}", { 'a' => a0 }) + end + + def test_output + a0 = [] + a1 = [1, "a", ["b", "c"], true] + assert_template_result("", "{{a}}", { 'a' => a0 }) + assert_template_result("1abctrue", "{{a}}", { 'a' => a1 }) + end +end diff --git a/test/integration/hash_test.rb b/test/integration/hash_test.rb new file mode 100644 index 00000000..a154c7a8 --- /dev/null +++ b/test/integration/hash_test.rb @@ -0,0 +1,223 @@ +# frozen_string_literal: true + +require "test_helper" +require "json" + +class HashTest < Minitest::Test + include Liquid + + class HashSubclass < Hash + def []=(key, value) + super(fmt_key(key), value) + end + + def [](key) + super(fmt_key(key)) + end + + def key?(key) + super(fmt_key(key)) + end + + def include?(key) + super(fmt_key(key)) + end + + def to_s + super.upcase + end + + private + + def fmt_key(key) + return key unless key.is_a?(String) + + key.upcase + end + end + + def test_size + assert_template_result("0", "{{h.size}}", { "h" => {} }) + assert_template_result("1", "{{h.size}}", { "h" => { "a" => 1 } }) + end + + def test_first + assert_template_result("", "{{h.first}}", { "h" => {} }) + assert_template_result("a1", "{{h.first}}", { "h" => { "a" => 1 } }) + end + + def test_last + assert_template_result("", "{{h.last}}", { "h" => { "a" => 1 } }) + end + + def test_index + assert_template_result("", "{{h['a']}}", { "h" => {} }) + assert_template_result("1", "{{h['a']}}", { "h" => { "a" => 1 } }) + end + + def test_to_s + h = { "a" => 1, "b" => [1, { "c" => 2 }] } + assert_template_result("{\"a\"=>1, \"b\"=>[1, {\"c\"=>2}]}", "{{h}}", { "h" => h }) + end + + def test_auto_methods + h1 = { "a" => 1 } + h2 = { "last" => 10, "first" => 11, "size" => 12 } + assert_template_result("1,nonblank", "{{h.size}},{%if h['size'] == blank%}blank{%else%}nonblank{%endif%}", { "h" => h1 }) + assert_template_result("12,nonblank", "{{h.size}},{%if h['size'] == blank%}blank{%else%}nonblank{%endif%}", { "h" => h2 }) + assert_template_result("a1,", "{{h.first}},{{h['first']}}", { "h" => h1 }) + assert_template_result("11,11", "{{h.first}},{{h['first']}}", { "h" => h2 }) + assert_template_result(",", "{{h.last}},{{h['last']}}", { "h" => h1 }) + assert_template_result("10,10", "{{h.last}},{{h['last']}}", { "h" => h2 }) + end + + def test_equality + h0 = {} + h1 = { "a" => 1 } + h2 = { "a" => 1, "b" => 2 } + h3 = { "a" => 1 } + + hashes = [h0, h1, h2, h3] + hashes.each_with_index do |a, i| + hashes.each_with_index do |b, j| + prefix = "(#{i},#{j})" + assert_template_result( + prefix + (a == b ? "y" : "n"), + "#{prefix}{% if a == b %}y{%else%}n{%endif%}", + { "a" => a, "b" => b }, + ) + end + end + end + + def test_contains + h = { "a" => 1, "b" => { "c" => "d" }, "3" => ["f", 2] } + + tpl = "{%if h contains b%}y{%else%}n{%endif%}" + + assert_template_result("y", tpl, { "h" => h, "b" => "a" }) + # assert_template_result("y", tpl, h, { "h" => { "b" => "a".html_safe } }) + assert_template_result("y", tpl, { "h" => h, "b" => "a".b }) + assert_template_result("n", tpl, { "h" => h, "b" => "A" }) + # assert_template_result("n", tpl, h, { "h" => { "b" => "A".html_safe } }) + assert_template_result("n", tpl, { "h" => h, "b" => "A".b }) + + assert_template_result("n", tpl, { "h" => h, "b" => 1 }) + assert_template_result("n", tpl, { "h" => h, "b" => "1" }) + + assert_template_result("n", tpl, { "h" => h, "b" => 3 }) + assert_template_result("y", tpl, { "h" => h, "b" => "3" }) + end + + def test_empty + assert_template_result("y", "{%if h == empty%}y{%else%}n{%endif%}", { "h" => {} }) + assert_template_result("n", "{%if h == empty%}y{%else%}n{%endif%}", { "h" => { "a" => 1 } }) + end + + def test_blank + assert_template_result("y", "{%if h == blank%}y{%else%}n{%endif%}", { "h" => {} }) + assert_template_result("n", "{%if h == blank%}y{%else%}n{%endif%}", { "h" => { "a" => 1 } }) + end + + def test_iter + h0 = { "a" => 1, "b" => { "c" => "d" }, "3" => ["f", 2] } + assert_template_result("a1b{\"c\"=>\"d\"}3f2", "{%for i in h%}{{i}}{%endfor%}", { "h" => h0 }) + end + + def test_output + h0 = {} + h1 = { "a" => 1, "b" => { "c" => "d" }, "3" => ["f", 2] } + assert_template_result("{}", "{{h}}", { "h" => h0 }) + assert_template_result("{\"a\"=>1, \"b\"=>{\"c\"=>\"d\"}, \"3\"=>[\"f\", 2]}", "{{h}}", { "h" => h1 }) + end + + def test_integer_key + h = { 2 => "huh" } + assert_template_result("huh", "{{h[2]}}", { "h" => h }) + assert_template_result("huh", "{{h[b]}}", { "h" => h, "b" => 2 }) + assert_template_result("", "{{h[b]}}", { "h" => h, "b" => "2" }) + end + + def test_nil_key + assert_hash_roundtrip(nil, nil) + refute_hash_roundtrip(nil, "nil") + refute_hash_roundtrip(nil, "null") + end + + def test_bool_key + assert_hash_roundtrip(true, true) + refute_hash_roundtrip(true, false) + refute_hash_roundtrip(true, "true") + refute_hash_roundtrip(true, nil) + end + + def test_int_key + assert_hash_roundtrip(1, 1) + refute_hash_roundtrip(1, 2) + refute_hash_roundtrip(1, "1") + refute_hash_roundtrip(1, 1.0000000000001) + end + + def test_string_key + assert_hash_roundtrip("word", "word") + # assert_hash_roundtrip("word", "word".html_safe) + assert_hash_roundtrip("word", "word".b) + refute_hash_roundtrip("word", "word ") + refute_hash_roundtrip("word", nil) + refute_hash_roundtrip("word", true) + end + + def test_weird_keys + assert_hash_roundtrip([1, 2], [1, 2]) + refute_hash_roundtrip([1, 2], [1, 3]) + assert_hash_roundtrip({ "a" => 1 }, { "a" => 1 }) + refute_hash_roundtrip({ "a" => 1 }, { "a" => 2 }) + o = Struct.new(:to_liquid).new(Object.new) + # failed_hash_roundtrip(o, o) + refute_hash_roundtrip(o, 1) + refute_hash_roundtrip(o, nil) + assert_hash_roundtrip("\xff".b, "\xff".b) + assert_hash_roundtrip(1..2, 1..2) + refute_hash_roundtrip(1..2, 1...2) + assert_hash_roundtrip( + 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111, + 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111, + ) + # f64 hashing isn't perfect but we can make some basic assertions. + assert_hash_roundtrip(1.1234, 1.1234) + refute_hash_roundtrip(1.1234, 1.1) + end + + def test_hash_html_key + # I mean this is probably not ideal but this is the way liquid works. + assert_template_result("{\"a