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
24 changes: 20 additions & 4 deletions lib/liquid/spec/cli/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def run_specs_frozen(config, options)
total_errors = 0
all_failures = []
max_failures = options[:max_failures]
max_passed_complexity = 0
results_by_complexity = Hash.new { |h, k| h[k] = { pass: 0, fail: 0, error: 0 } }

suites_to_run.each do |suite|

Expand All @@ -248,6 +248,8 @@ def run_specs_frozen(config, options)
errors = 0

suite_specs.each do |spec|
complexity = spec.complexity || 1000

begin
result = run_single_spec(spec, config)
rescue SystemExit, Interrupt, SignalException
Expand All @@ -259,13 +261,14 @@ def run_specs_frozen(config, options)
case result[:status]
when :pass
passed += 1
complexity = spec.complexity || 0
max_passed_complexity = complexity if complexity > max_passed_complexity
results_by_complexity[complexity][:pass] += 1
when :fail
failed += 1
results_by_complexity[complexity][:fail] += 1
all_failures << { spec: spec, result: result }
when :error
errors += 1
results_by_complexity[complexity][:error] += 1
all_failures << { spec: spec, result: result }
end
end
Expand All @@ -289,8 +292,21 @@ def run_specs_frozen(config, options)

print_failures(all_failures, max_failures)

# Calculate max complexity reached (highest level where all specs pass)
sorted_complexities = results_by_complexity.keys.sort
max_complexity_reached = 0
sorted_complexities.each do |c|
r = results_by_complexity[c]
if r[:fail] == 0 && r[:error] == 0
max_complexity_reached = c
else
break
end
end
max_possible = sorted_complexities.reject { |c| c >= 1000 }.max || 0

puts ""
puts "Total: #{total_passed} passed, #{total_failed} failed, #{total_errors} errors (max complexity: #{max_passed_complexity})"
puts "Total: #{total_passed} passed, #{total_failed} failed, #{total_errors} errors. Max complexity reached: #{max_complexity_reached}/#{max_possible}"

exit(1) if all_failures.any?
end
Expand Down
80 changes: 37 additions & 43 deletions specs/basics/blank-and-empty.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@
empty. This tests that your empty check works across different
collection types (arrays and objects).

- name: "nil_is_empty"
- name: "nil_is_not_empty"
template: "{% if missing == empty %}empty{% else %}not{% endif %}"
environment: {}
expected: "empty"
expected: "not"
complexity: 110
hint: |
Nil/undefined values should equal 'empty'. When a variable
doesn't exist in the context, it evaluates to nil, and nil
should be considered empty. This is a key Liquid semantic:
nil is a subset of empty - all nil values are empty.
In liquid-ruby, nil does NOT equal empty. Undefined variables
evaluate to nil, but nil is a distinct concept from empty.
The 'empty' keyword refers to empty strings "", not nil values.
Use nil check for undefined, empty check for empty strings.

- name: "nil_is_also_nil"
template: "{% if missing == nil %}nil{% else %}not{% endif %}"
Expand Down Expand Up @@ -80,38 +80,35 @@
with spaces, tabs, or newlines has length > 0, so it's not
empty. This distinguishes 'empty' from 'blank'.

- name: "whitespace_string_is_blank"
- name: "whitespace_string_not_blank_in_liquid_ruby"
template: "{% if text == blank %}blank{% else %}not{% endif %}"
environment: { text: " " }
expected: "blank"
expected: "not"
complexity: 120
hint: |
The 'blank' keyword is stricter than 'empty'. Recognize 'blank'
as a keyword. A value is blank if it's empty OR contains only
whitespace. Implement blank checking: trim the string and check
if the result is empty. Whitespace-only strings are blank.
In liquid-ruby, the 'blank' keyword does not implement equality
comparison. Nothing equals blank, not even blank itself.
The blank keyword exists but doesn't function for comparisons.

- name: "empty_string_is_blank"
- name: "empty_string_not_blank_in_liquid_ruby"
template: "{% if x == blank %}blank{% else %}not{% endif %}"
environment: { x: "" }
expected: "blank"
expected: "not"
complexity: 120
hint: |
Empty strings are also blank. The 'blank' condition is a
superset of 'empty' for strings. All empty strings are blank,
but not all blank strings are empty (whitespace-only strings
are blank but not empty in the length sense).
In liquid-ruby, empty strings don't equal blank because blank
doesn't implement equality comparison. Use 'empty' instead
for checking empty strings: x == empty works correctly.

- name: "nil_is_blank"
- name: "nil_not_blank_in_liquid_ruby"
template: "{% if missing == blank %}blank{% else %}not{% endif %}"
environment: {}
expected: "blank"
expected: "not"
complexity: 120
hint: |
Nil values are considered blank. The hierarchy is: nil ⊂ empty ⊂ blank.
All nil values are empty, and all empty values are blank (for most
types). When checking for blank, nil should return true, just as
it does for empty.
In liquid-ruby, blank doesn't implement equality comparison.
Nothing equals blank, including nil/undefined values.
Use nil check instead: missing == nil.

- name: "false_is_not_empty"
template: "{% if flag == empty %}empty{% else %}not{% endif %}"
Expand All @@ -124,16 +121,15 @@
or zero-length collections. When checking == empty, false should
not match. Only nil, "", [], and {} are empty.

- name: "false_is_blank"
- name: "false_not_blank_in_liquid_ruby"
template: "{% if flag == blank %}blank{% else %}not{% endif %}"
environment: { flag: false }
expected: "blank"
expected: "not"
complexity: 130
hint: |
The boolean false IS blank. This is a Liquid quirk: false and
nil are considered blank, even though false is not empty. The
blank check is more about "falsiness" than just empty collections.
Implement: nil, false, "", whitespace-only, [], {} are all blank.
In liquid-ruby, blank doesn't implement equality comparison.
Nothing equals blank, including false. The blank keyword exists
in liquid-ruby but doesn't function for comparisons.

- name: "zero_is_not_empty"
template: "{% if num == empty %}empty{% else %}not{% endif %}"
Expand Down Expand Up @@ -179,16 +175,15 @@
The property access happens first, then the empty check. This
tests that empty works with any expression, not just top-level vars.

- name: "nil_property_is_empty"
- name: "nil_property_is_not_empty_in_liquid_ruby"
template: "{% if user.missing == empty %}empty{% else %}not{% endif %}"
environment: { user: {} }
expected: "empty"
expected: "not"
complexity: 140
hint: |
Accessing a missing property returns nil, which is empty.
When user.missing is evaluated, the property doesn't exist,
so it returns nil. Then nil == empty evaluates to true.
This combines property access with empty checking.
In liquid-ruby, nil does not equal empty. Accessing a missing
property returns nil, and nil == empty evaluates to false.
Other implementations may treat nil as empty.

- name: "default_with_empty_check"
template: "{{ text | default: 'N/A' }}"
Expand Down Expand Up @@ -259,17 +254,16 @@
empty string. The AND requires both conditions true, so this
evaluates false, outputting "Anonymous". Guard against nested empties.

- name: "empty_vs_blank_comparison"
- name: "empty_vs_blank_comparison_in_liquid_ruby"
template: "{% if text == empty %}E{% elsif text == blank %}B{% else %}N{% endif %}"
environment: { text: " \n " }
expected: "B"
expected: "N"
complexity: 170
hint: |
Show the distinction between empty and blank with cascading checks.
A whitespace-with-newlines string is not empty (has characters)
but is blank (only whitespace). Check empty first (false), then
blank (true). This demonstrates the semantic hierarchy where
blank is more permissive than empty for strings.
In liquid-ruby, blank doesn't implement equality comparison.
A whitespace-only string is not equal to empty (has characters),
and text == blank is always false. Other implementations may
support blank comparisons where whitespace-only strings equal blank.

- name: "array_size_vs_empty"
template: "{% if items.size == 0 %}size zero{% elsif items == empty %}empty{% else %}has items{% endif %}"
Expand Down
54 changes: 23 additions & 31 deletions specs/basics/error-handling.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,16 @@
in the context. This is fundamental to Liquid's forgiving error handling.
Do not throw errors or warnings for missing variables in lax mode.

- name: "undefined_variable_strict"
- name: "undefined_variable_strict_in_liquid_ruby"
template: "{{ missing_var }}"
environment: {}
expected: "Liquid error: undefined variable 'missing_var'"
expected: ""
complexity: 230
error_mode: strict
render_errors: true
hint: |
In strict mode, accessing undefined variables should raise an error.
The error should be caught and rendered inline as "Liquid error: [message]".
Strict mode helps catch typos and missing data during development.
Implement error handling that captures the variable name and context
for helpful error messages.
In liquid-ruby, strict mode only affects parsing, not runtime variable
lookup. Undefined variables still render as empty strings. Other
implementations may raise errors for undefined variables in strict mode.

- name: "property_access_on_nil_lax"
template: "{{ missing.property }}"
Expand All @@ -75,19 +72,16 @@
checks if the base is nil before attempting to access. Prevents null
pointer exceptions in templates.

- name: "property_access_on_nil_strict"
- name: "property_access_on_nil_strict_in_liquid_ruby"
template: "{{ missing.property }}"
environment: {}
expected: "Liquid error: undefined variable 'missing'"
expected: ""
complexity: 245
error_mode: strict
render_errors: true
hint: |
In strict mode, the error should occur at the variable lookup level.
Since 'missing' is undefined, the error is raised before attempting
property access. Strict mode validates each step of the lookup chain.
The error message should identify which variable is undefined for
easier debugging.
In liquid-ruby, strict mode only affects parsing. Property access on
undefined variables still returns nil (empty output). Other implementations
may raise errors for property access on undefined variables in strict mode.

- name: "property_access_on_non_object_lax"
template: "{{ number.property }}"
Expand Down Expand Up @@ -127,17 +121,15 @@
the original value as if the unknown filter wasn't there. This is critical
for backward compatibility.

- name: "unknown_filter_strict"
- name: "unknown_filter_strict_in_liquid_ruby"
template: "{{ 'hello' | nonexistent_filter }}"
expected: "Liquid error: unknown filter 'nonexistent_filter'"
expected: "hello"
complexity: 270
error_mode: strict
render_errors: true
hint: |
In strict mode, unknown filters should raise an error that gets rendered
inline. This helps catch typos in filter names during development. The
filter lookup system should check if the filter exists before attempting
to invoke it. Provide the filter name in the error message for debugging.
In liquid-ruby, unknown filters are ignored even in strict mode.
The filter passes through its input unchanged, like lax mode.
Other implementations may raise errors for unknown filters in strict mode.

- name: "filter_with_invalid_argument_lax"
template: "{{ 'hello' | slice: 'not a number' }}"
Expand Down Expand Up @@ -175,17 +167,17 @@
recognize the tag syntax but the parser treats unknown tags as raw output.
Include both opening and closing tags in the raw output.

- name: "unknown_tag_strict"
- name: "unknown_tag_strict_parse_error"
template: "{% nonexistent_tag %}content{% endnonexistent_tag %}"
expected: "Liquid error: unknown tag 'nonexistent_tag'"
complexity: 300
error_mode: strict
render_errors: true
errors:
parse_error:
- "Unknown tag"
hint: |
In strict mode, unknown tags should raise an error immediately during
parsing or rendering. The tag registry lookup fails and an error is raised.
This prevents templates with typos from silently failing. The error should
identify the unknown tag name and ideally the line number where it appears.
In liquid-ruby strict mode, unknown tags cause a parse error rather than
a render error. The template fails to parse with "Unknown tag 'nonexistent_tag'".
This is stricter than lax mode which outputs tags as raw text.

- name: "malformed_syntax_unclosed_tag_lax"
template: "{% if true %}hello"
Expand Down Expand Up @@ -296,7 +288,7 @@

- name: "strict_mode_with_nil_safe_access"
template: "{{ user.profile.name | default: 'Anonymous' }}"
environment: { user: { profile: nil } }
environment: { user: { profile: null } }
expected: "Anonymous"
complexity: 295
error_mode: strict
Expand Down
18 changes: 9 additions & 9 deletions specs/basics/expressions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@
- name: "nested_bracket_access"
template: "{{ matrix[0][1] }}"
environment: { matrix: [[1, 2, 3], [4, 5, 6]] }
expected: "5"
expected: "2"
complexity: 95
hint: |
Parse chained bracket notation. First access matrix[0] to
get [4, 5, 6], then access [1] on that result to get 5.
get [1, 2, 3], then access [1] on that result to get 2.
Build nested BracketAccess nodes. Evaluate inside-out or
left-to-right, passing results forward.

Expand Down Expand Up @@ -271,14 +271,14 @@
- name: "range_in_bracket"
template: "{{ numbers[(1..3)] }}"
environment: { numbers: { "1..3": "range" } }
expected: "range"
expected: ""
complexity: 150
hint: |
Distinguish between range creation and range as a key.
Without parentheses around the range, this is tricky.
In this case, (1..3) in brackets evaluates to a range object,
which becomes a string key "1..3" for hash access. This tests
expression evaluation contexts.
In liquid-ruby, a range expression in bracket access is not
stringified for key lookup. The range object (1..3) is used
directly, which doesn't match the string key "1..3". This
results in nil (empty output). Other implementations may
stringify the range for hash key lookup.

- name: "nil_safe_navigation"
template: "{{ missing.property }}"
Expand Down Expand Up @@ -322,7 +322,7 @@
Create ranges using variable expressions for both bounds.
Parse (x..y) where x and y are identifiers. During evaluation,
resolve both to numbers (2 and 4), then generate the range
[2, 3, 4]. This enables dynamic range creation.
array [2, 3, 4] which outputs as "234".

- name: "multiple_bracket_lookups"
template: "{{ a[b][c] }}"
Expand Down
Loading
Loading