Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ out of regular runs.

| Suite | Tests | Description |
|-------|-------|-------------|
| **basics** | 945 | Essential Liquid features - start here! Ordered by complexity with implementation hints |
| **liquid_ruby** | 2,097 | Core Liquid specs from [Shopify/liquid](https://github.com/Shopify/liquid) integration tests |
| **basics** | 955 | Essential Liquid features - start here! Ordered by complexity with implementation hints |
| **liquid_ruby** | 2,106 | Core Liquid specs from [Shopify/liquid](https://github.com/Shopify/liquid) integration tests |
| **liquid_ruby_lax** | 121 | Lax-mode reference behavior |
| **parser_errors** | 1,905 | Strict parser error compatibility and mutation matrices |
| **partials** | 12 | Include/render focused compatibility specs and timings |
Expand Down
2 changes: 1 addition & 1 deletion examples/json_rpc_ruby_liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
config.render_error_modes = [:raise, :inline]
# This JSON-RPC adapter supports core behavior including drops.
# because they implement bidirectional callbacks
config.missing_features = [:ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :strict2_blank_body_errors]
config.missing_features = [:ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :range_resource_limits, :strict2_blank_body_errors]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_c.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def parse_expression(markup, safe: false)
LiquidSpec.configure do |config|
config.error_modes = [:lax]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:drops, :ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access]
config.missing_features = [:drops, :ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :range_resource_limits]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_c_strict.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
LiquidSpec.configure do |config|
config.error_modes = [:strict]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:drops, :ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access]
config.missing_features = [:drops, :ruby_types, :ruby_drops, :binary_data, :template_factory, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :range_resource_limits]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
LiquidSpec.configure do |config|
config.error_modes = [:strict2, :strict]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:self_environment_shadowing, :drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :strict2_blank_body_errors]
config.missing_features = [:self_environment_shadowing, :drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :range_resource_limits, :strict2_blank_body_errors]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_ruby_lax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
LiquidSpec.configure do |config|
config.error_modes = [:lax]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :range_resource_limits]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_ruby_shopify.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def render_to_output_buffer(context, output)
config.render_error_modes = [:raise, :inline]
# This adapter is a benchmark fixture with a few local theme filters, not a
# full Shopify runtime. Keep production/theme suites out of its matrix row.
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits, :range_resource_limits]
config.suites = [:benchmarks]
config.filter = "shopify_"
end
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_ruby_yjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
LiquidSpec.configure do |config|
config.error_modes = [:strict2, :strict]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits, :strict2_blank_body_errors]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits, :range_resource_limits, :strict2_blank_body_errors]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
2 changes: 1 addition & 1 deletion examples/liquid_ruby_zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
LiquidSpec.configure do |config|
config.error_modes = [:strict2, :strict]
config.render_error_modes = [:raise, :inline]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits, :strict2_blank_body_errors]
config.missing_features = [:drop_class_output, :shopify_tags, :shopify_objects, :shopify_filters, :shopify_includes, :shopify_blank, :shopify_error_handling, :shopify_error_format, :shopify_string_access, :shopify_resource_limits, :range_resource_limits, :strict2_blank_body_errors]
end

LiquidSpec.compile do |ctx, source, parse_options|
Expand Down
1 change: 1 addition & 0 deletions lib/liquid/spec/cli/adapter_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def format_timeout(value)
strict2_blank_body_errors: "strict2 surfaces inline errors even when the block body is blank",
drops: "Standard test drop library (BooleanDrop, MethodDrop, SequenceDrop, etc.)",
shopify_resource_limits: "Shopify resource limit enforcement (render score tracking, cumulative limits across partials)",
range_resource_limits: "Post-5.13 range iteration resource-limit accounting",
randomness: "Specs using generated random values for increased coverage",
}.freeze
class Configuration
Expand Down
5 changes: 5 additions & 0 deletions lib/liquid/spec/cli/features.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ module Features
recommendation: :optional,
note: "Score bookkeeping and limit enforcement is implementation plumbing, not Liquid semantics. Recursion depth / stack overflow specs are core; score-counting specs are opt-out.",
},
range_resource_limits: {
description: "Post-5.13 range iteration resource-limit accounting",
recommendation: :optional,
note: "Requires the range iteration accounting introduced after Liquid 5.13. Keep it disabled until that behavior is implemented.",
},
}.freeze

RECOMMENDATION_LABELS = {
Expand Down
1 change: 1 addition & 0 deletions lib/liquid/spec/cli/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ module Init
:shopify_error_handling,
:shopify_error_format,
:shopify_string_access,
:range_resource_limits,
:strict2_blank_body_errors,
]
end
Expand Down
9 changes: 5 additions & 4 deletions lib/liquid/spec/deps/liquid_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ def to_liquid
class RangeWrapper
def self.new(params)
if params.is_a?(Array)
Range.new(params[0], params[1])
Range.new(params[0], params[1], params[2] || false)
elsif params.is_a?(Hash)
Range.new(params["begin"] || params[:begin], params["end"] || params[:end])
Range.new(params["begin"] || params[:begin], params["end"] || params[:end], params["exclude_end"] || params[:exclude_end] || false)
else
params
end
Expand Down Expand Up @@ -551,8 +551,9 @@ def secret; "FAKE_SECRET"; end
Liquid::Spec::ClassRegistry.register("FakeDropObject") { |p| FakeDropObject.new }


# Range - special handling for array format [start, end]
Liquid::Spec::ClassRegistry.register("Range") { |p| Range.new(p[0], p[1]) }
# Range - array format [start, end] retains the historic inclusive encoding;
# a third truthy item preserves an exclusive endpoint.
Liquid::Spec::ClassRegistry.register("Range") { |p| Range.new(p[0], p[1], p[2] || false) }

# Returns the Liquid::Drop class itself (for edge case tests)
Liquid::Spec::ClassRegistry.register("LiquidDropClass") { |_p| Liquid::Drop }
Expand Down
8 changes: 6 additions & 2 deletions lib/liquid/spec/deps/yaml_coders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@

# --- Core type coders ---

# Range serializes as instantiate:Range: [begin, end]
# Inclusive ranges retain the historical [begin, end] representation. Exclusive
# ranges append true so their endpoint semantics survive capture; nil end supports
# endless ranges without a special representation.
class Range
def encode_with(coder)
coder.represent_map(nil, { "instantiate:Range:" => [self.begin, self.end] })
params = [self.begin, self.end]
params << true if exclude_end?
coder.represent_map(nil, { "instantiate:Range:" => params })
end
end

Expand Down
1 change: 1 addition & 0 deletions scripts/verifiers/spec_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
drops
randomness
shopify_resource_limits
range_resource_limits
activesupport
].freeze

Expand Down
146 changes: 146 additions & 0 deletions specs/basics/range-resource-limits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
_metadata:
hint: |
Regression coverage for bounded range iteration's resource-limit accounting.
These cases require the post-5.13 range resource-limit implementation in
addition to Shopify resource-limit support.
complexity: 500
features:
- shopify_resource_limits
- range_resource_limits

specs:
- name: for_dynamic_range_empty_body_charges_each_item
complexity: 500
template: "{% for i in (first..last) %}{% endfor %}"
environment:
first: 1
last: 3
error_mode: [strict2, strict]
render_errors: false
resource_limits:
render_score_limit: 3
errors:
render_error:
- Memory limits exceeded
hint: |
The root loop tag costs one render-score unit and its three yielded values
cost three more, even when the body is empty. Dynamic bounds must use the
same accounting as a literal range, so the four-unit loop exceeds 3.

- name: tablerow_literal_range_empty_body_charges_each_item
complexity: 501
template: "{% tablerow i in (1..3) %}{% endtablerow %}"
error_mode: [strict2, strict]
render_errors: false
resource_limits:
render_score_limit: 3
errors:
render_error:
- Memory limits exceeded
hint: |
tablerow also charges its tag plus every range item before rendering the
body. Empty cells still consume work, so the fourth unit exceeds this
render-score limit.

- name: for_literal_range_reversed_offset_limit_keeps_metadata
complexity: 502
template: "{% for i in (1..10) reversed offset:7 limit:2 %}{{ forloop.length }}:{{ forloop.index }}:{{ i }}{% endfor %}"
expected: "2:1:92:2:8"
hint: |
Apply offset and limit to the range segment without materializing it, then
reverse that segment. forloop.length and forloop.index describe the two
selected values (9 and 8), not the original ten-item range.

- name: tablerow_literal_range_offset_limit_keeps_metadata
complexity: 503
template: "{% tablerow i in (1..10) offset:7 limit:2 %}{{ tablerowloop.length }}:{{ tablerowloop.index }}:{{ i }}{% endtablerow %}"
expected: |
<tr class="row1">
<td class="col1">2:1:8</td><td class="col2">2:2:9</td></tr>
hint: |
tablerow selects a bounded range segment before generating markup. Its
loop metadata reports the two selected values (8 and 9), including their
one-based indexes, rather than the full range length.

- name: range_score_is_cumulative_across_isolated_render_partials
complexity: 504
template: "{% render 'range_loop' %}{% render 'range_loop' %}"
error_mode: [strict2, strict]
render_errors: false
filesystem:
range_loop.liquid: "{% for i in (1..2) %}{% endfor %}"
resource_limits:
cumulative_render_score_limit: 5
errors:
render_error:
- Memory limits exceeded
hint: |
render creates an isolated Liquid context, but its work contributes to the
caller's cumulative render score. Two empty two-item range loops cost more
than 5 together even though neither partial has its own per-render limit.

- name: tablerow_capture_output_respects_assign_score_limit
complexity: 505
template: "{% capture table %}{% tablerow i in (1..2) %}{{ i }}{% endtablerow %}{% endcapture %}"
error_mode: [strict2, strict]
render_errors: false
resource_limits:
assign_score_limit: 69
errors:
render_error:
- Memory limits exceeded
hint: |
Capturing tablerow's generated HTML adds all 70 bytes to assign score even
though capture emits no final output. The limit is checked against the
complete generated table, not just the tag body.

- name: tablerow_break_final_markup_respects_render_length_limit
complexity: 506
template: "{% tablerow i in (1..3) %}{{ i }}{% break %}{% endtablerow %}"
error_mode: [strict2, strict]
render_errors: false
resource_limits:
render_length_limit: 46
errors:
render_error:
- Memory limits exceeded
hint: |
break stops further cells but tablerow must still close the current cell
and row. The resulting 47-byte final markup is subject to the render
length limit, so a limit of 46 raises a memory error.

- name: for_nonempty_range_at_render_score_boundary
complexity: 507
template: "{% for i in (1..3) %}{{ i }}{% endfor %}"
resource_limits:
render_score_limit: 7
expected: "123"
hint: |
A three-item range loop with one rendered node per item costs seven render
score units: one loop tag, three yielded values, and three body renders.
The exact boundary succeeds and produces all three values.

- name: for_nonempty_range_over_render_score_boundary
complexity: 508
template: "{% for i in (1..3) %}{{ i }}{% endfor %}"
error_mode: [strict2, strict]
render_errors: false
resource_limits:
render_score_limit: 6
errors:
render_error:
- Memory limits exceeded
hint: |
The same seven-unit nonempty range loop must raise when the render-score
limit is six. Check every yielded value and body render against the limit.

- name: for_offset_range_does_not_charge_skipped_items
complexity: 509
template: "{% for i in (1..10) offset:7 limit:2 %}{{ i }}{% endfor %}"
resource_limits:
render_score_limit: 5
expected: "89"
hint: |
Offset and limit select only 8 and 9 from this bounded native integer
range. Their five score units are the loop tag, two selected values, and
two body renders; the seven skipped values must not be charged.
6 changes: 4 additions & 2 deletions specs/basics/resource-limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ specs:
render_error:
- Liquid error
hint: |
Each partial renders a for-loop scoring ~10 points. Per-template limit (100)
is fine, but cumulative limit (25) is exceeded after the second partial.
Each nonempty ten-item partial loop costs 21 render-score units: one loop
tag, ten yielded values, and ten body renders. The caller precharges its
three render nodes, so the first partial reaches 24 and the second exceeds
the cumulative limit of 25 while it is rendering.

- name: cumulative_limits_not_triggered_when_unset
complexity: 1000
Expand Down
Loading
Loading