mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 15:30:39 -07:00
Add targeted tests for byte-walking fast paths
36 tests covering the three optimization sites:
Expression.parse_number (13 tests):
- Simple integers, negatives, floats, trailing dots
- Multi-dot truncation (1.2.3 → 1.2)
- Rejection of non-numeric input and trailing alpha (1.2.3a)
Expression.parse strip guard (7 tests):
- Leading, trailing, both-sides whitespace
- No-strip-needed case (no allocation)
- Null byte stripping (matches String#strip behavior)
VariableLookup.simple_lookup? (8 tests):
- Accepts: single names, dotted chains, question marks, hyphens
- Rejects: brackets, empty, leading/trailing dots, double dots
VariableLookup fast path equivalence (7 tests):
- name/lookups/command_flags match for simple and deep chains
- Bracket inputs fall through to regex path correctly
BlockBody.try_parse_tag_token (10 tests):
- Simple tags, whitespace control variants ({%-, -%}, both)
- No-markup tags, hash comments, newline counting
- Hyphenated names stop at hyphen (matching TagName = /\w+/)
- Malformed tokens return nil (fallback to FullToken regex)
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
# Tests for the byte-walking fast paths introduced in the ByteTables optimization.
|
||||
# Each fast path is tested for equivalence with the original regex-based code path
|
||||
# it replaces, covering accepted inputs, rejected inputs, and boundary cases.
|
||||
class FastPathUnitTest < Minitest::Test
|
||||
# ── Expression.parse_number ────────────────────────────────────────
|
||||
# Fast path: byte-walk with ByteTables::DIGIT
|
||||
# Replaces: INTEGER_REGEX, FLOAT_REGEX, StringScanner loop
|
||||
|
||||
def test_parse_number_simple_integers
|
||||
assert_equal(42, Liquid::Expression.parse_number("42"))
|
||||
assert_equal(0, Liquid::Expression.parse_number("0"))
|
||||
assert_equal(999, Liquid::Expression.parse_number("999"))
|
||||
end
|
||||
|
||||
def test_parse_number_negative_integers
|
||||
assert_equal(-7, Liquid::Expression.parse_number("-7"))
|
||||
assert_equal(-0, Liquid::Expression.parse_number("-0"))
|
||||
assert_equal(-123, Liquid::Expression.parse_number("-123"))
|
||||
end
|
||||
|
||||
def test_parse_number_simple_floats
|
||||
assert_equal(3.14, Liquid::Expression.parse_number("3.14"))
|
||||
assert_equal(0.5, Liquid::Expression.parse_number("0.5"))
|
||||
assert_equal(-0.5, Liquid::Expression.parse_number("-0.5"))
|
||||
assert_equal(100.0, Liquid::Expression.parse_number("100.0"))
|
||||
end
|
||||
|
||||
def test_parse_number_trailing_dot
|
||||
# "123." → 123.0 (truncate before dot)
|
||||
assert_equal(123.0, Liquid::Expression.parse_number("123."))
|
||||
assert_equal(0.0, Liquid::Expression.parse_number("0."))
|
||||
end
|
||||
|
||||
def test_parse_number_multi_dot_floats
|
||||
# "1.2.3" → 1.2 (truncate at second dot)
|
||||
assert_equal(1.2, Liquid::Expression.parse_number("1.2.3"))
|
||||
assert_equal(1.2, Liquid::Expression.parse_number("1.2.3.4"))
|
||||
assert_equal(0.0, Liquid::Expression.parse_number("0.0.0"))
|
||||
end
|
||||
|
||||
def test_parse_number_rejects_non_numeric
|
||||
assert_nil(Liquid::Expression.parse_number("hello"))
|
||||
assert_nil(Liquid::Expression.parse_number(""))
|
||||
assert_nil(Liquid::Expression.parse_number("abc123"))
|
||||
assert_nil(Liquid::Expression.parse_number(".5"))
|
||||
assert_nil(Liquid::Expression.parse_number("-.5"))
|
||||
end
|
||||
|
||||
def test_parse_number_rejects_trailing_alpha_after_multi_dot
|
||||
# "1.2.3a" must be nil, not 1.2 — these are not valid numbers
|
||||
assert_nil(Liquid::Expression.parse_number("1.2.3a"))
|
||||
assert_nil(Liquid::Expression.parse_number("1.2.3.4a"))
|
||||
assert_nil(Liquid::Expression.parse_number("1.2.34a"))
|
||||
assert_nil(Liquid::Expression.parse_number("-1.2.3a"))
|
||||
end
|
||||
|
||||
def test_parse_number_rejects_bare_dash
|
||||
assert_nil(Liquid::Expression.parse_number("-"))
|
||||
assert_nil(Liquid::Expression.parse_number("-a"))
|
||||
end
|
||||
|
||||
# ── Expression.parse strip guard ───────────────────────────────────
|
||||
# Fast path: skip String#strip when no leading/trailing whitespace
|
||||
# Must produce identical results to unconditional .strip
|
||||
|
||||
def test_parse_strips_leading_whitespace
|
||||
assert_equal(42, Liquid::Expression.parse(" 42"))
|
||||
assert_equal(42, Liquid::Expression.parse("\t42"))
|
||||
assert_equal(42, Liquid::Expression.parse("\n42"))
|
||||
end
|
||||
|
||||
def test_parse_strips_trailing_whitespace
|
||||
assert_equal(42, Liquid::Expression.parse("42 "))
|
||||
assert_equal(42, Liquid::Expression.parse("42\t"))
|
||||
assert_equal(42, Liquid::Expression.parse("42\n"))
|
||||
end
|
||||
|
||||
def test_parse_strips_both_sides
|
||||
assert_equal(42, Liquid::Expression.parse(" 42 "))
|
||||
assert_equal("hello", Liquid::Expression.parse(" 'hello' "))
|
||||
end
|
||||
|
||||
def test_parse_no_strip_needed
|
||||
assert_equal(42, Liquid::Expression.parse("42"))
|
||||
assert_equal("hello", Liquid::Expression.parse("'hello'"))
|
||||
assert_equal(true, Liquid::Expression.parse("true"))
|
||||
end
|
||||
|
||||
def test_parse_strips_null_bytes
|
||||
# String#strip removes \x00 — the WHITESPACE table must match
|
||||
assert_equal(true, Liquid::Expression.parse("\x00true"))
|
||||
assert_equal(true, Liquid::Expression.parse("true\x00"))
|
||||
end
|
||||
|
||||
# ── VariableLookup.simple_lookup? ──────────────────────────────────
|
||||
# Fast path: regex gate for simple a.b.c chains
|
||||
# Must accept only inputs the byte-walk can handle correctly
|
||||
|
||||
def test_simple_lookup_accepts_single_names
|
||||
assert(Liquid::VariableLookup.simple_lookup?("product"))
|
||||
assert(Liquid::VariableLookup.simple_lookup?("x"))
|
||||
assert(Liquid::VariableLookup.simple_lookup?("_private"))
|
||||
end
|
||||
|
||||
def test_simple_lookup_accepts_dotted_chains
|
||||
assert(Liquid::VariableLookup.simple_lookup?("product.title"))
|
||||
assert(Liquid::VariableLookup.simple_lookup?("a.b.c.d"))
|
||||
end
|
||||
|
||||
def test_simple_lookup_accepts_question_marks
|
||||
assert(Liquid::VariableLookup.simple_lookup?("product.available?"))
|
||||
assert(Liquid::VariableLookup.simple_lookup?("empty?"))
|
||||
end
|
||||
|
||||
def test_simple_lookup_accepts_hyphens
|
||||
assert(Liquid::VariableLookup.simple_lookup?("my-var"))
|
||||
assert(Liquid::VariableLookup.simple_lookup?("my-var.some-field"))
|
||||
end
|
||||
|
||||
def test_simple_lookup_rejects_brackets
|
||||
refute(Liquid::VariableLookup.simple_lookup?("product[0]"))
|
||||
refute(Liquid::VariableLookup.simple_lookup?("hash['key']"))
|
||||
refute(Liquid::VariableLookup.simple_lookup?("[0]"))
|
||||
end
|
||||
|
||||
def test_simple_lookup_rejects_empty_and_malformed
|
||||
refute(Liquid::VariableLookup.simple_lookup?(""))
|
||||
refute(Liquid::VariableLookup.simple_lookup?(".leading"))
|
||||
refute(Liquid::VariableLookup.simple_lookup?("trailing."))
|
||||
refute(Liquid::VariableLookup.simple_lookup?("a..b"))
|
||||
end
|
||||
|
||||
# ── VariableLookup fast path equivalence ───────────────────────────
|
||||
# The fast path must produce identical name, lookups, and command_flags
|
||||
# to the original VariableParser regex path
|
||||
|
||||
def test_fast_path_simple_name
|
||||
vl = Liquid::VariableLookup.new("product")
|
||||
assert_equal("product", vl.name)
|
||||
assert_equal([], vl.lookups)
|
||||
end
|
||||
|
||||
def test_fast_path_dotted_chain
|
||||
vl = Liquid::VariableLookup.new("product.title")
|
||||
assert_equal("product", vl.name)
|
||||
assert_equal(["title"], vl.lookups)
|
||||
end
|
||||
|
||||
def test_fast_path_deep_chain
|
||||
vl = Liquid::VariableLookup.new("a.b.c.d")
|
||||
assert_equal("a", vl.name)
|
||||
assert_equal(["b", "c", "d"], vl.lookups)
|
||||
end
|
||||
|
||||
def test_fast_path_command_methods
|
||||
vl = Liquid::VariableLookup.new("items.size")
|
||||
assert_equal("items", vl.name)
|
||||
assert_equal(["size"], vl.lookups)
|
||||
assert(vl.lookup_command?(0))
|
||||
|
||||
vl2 = Liquid::VariableLookup.new("items.first")
|
||||
assert(vl2.lookup_command?(0))
|
||||
|
||||
vl3 = Liquid::VariableLookup.new("items.last")
|
||||
assert(vl3.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_fast_path_non_command_lookups
|
||||
vl = Liquid::VariableLookup.new("product.title")
|
||||
refute(vl.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_fast_path_question_mark
|
||||
vl = Liquid::VariableLookup.new("product.available?")
|
||||
assert_equal("product", vl.name)
|
||||
assert_equal(["available?"], vl.lookups)
|
||||
end
|
||||
|
||||
def test_bracket_lookup_falls_to_regex_path
|
||||
vl = Liquid::VariableLookup.new("product[0]")
|
||||
assert_equal("product", vl.name)
|
||||
assert_equal([0], vl.lookups)
|
||||
end
|
||||
|
||||
# ── BlockBody.try_parse_tag_token ──────────────────────────────────
|
||||
# Fast path: byte-walk tag tokens instead of FullToken regex
|
||||
# Must produce identical [tag_name, markup, newline_count] or nil
|
||||
|
||||
def test_tag_token_simple
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% if x %}")
|
||||
assert_equal(["if", "x ", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_whitespace_control_leading
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{%- if x %}")
|
||||
assert_equal(["if", "x ", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_whitespace_control_trailing
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% if x -%}")
|
||||
assert_equal(["if", "x ", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_whitespace_control_both
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{%- if x -%}")
|
||||
assert_equal(["if", "x ", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_no_markup
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% endif %}")
|
||||
assert_equal(["endif", "", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_hash_comment
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% # this is a comment %}")
|
||||
assert_equal(["#", "this is a comment ", 0], result)
|
||||
end
|
||||
|
||||
def test_tag_token_with_newlines
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% \n if \n x %}")
|
||||
assert_equal(["if", "x ", 2], result)
|
||||
end
|
||||
|
||||
def test_tag_token_hyphenated_name_stops_at_hyphen
|
||||
# TagName = /\w+/ does not include hyphens
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% my-tag markup %}")
|
||||
assert_equal("my", result[0])
|
||||
end
|
||||
|
||||
def test_tag_token_complex_markup
|
||||
body = Liquid::BlockBody.new
|
||||
result = body.send(:try_parse_tag_token, "{% for item in collection reversed %}")
|
||||
assert_equal("for", result[0])
|
||||
assert_equal("item in collection reversed ", result[1])
|
||||
end
|
||||
|
||||
def test_tag_token_malformed_returns_nil
|
||||
body = Liquid::BlockBody.new
|
||||
# Token too short
|
||||
assert_nil(body.send(:try_parse_tag_token, "{%"))
|
||||
# No valid tag name start (digit)
|
||||
assert_nil(body.send(:try_parse_tag_token, "{% 123 %}"))
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user