Compare commits

...

18 Commits

Author SHA1 Message Date
Samuel
fd5e3e87c7 Optimize range iteration
For loops support a range syntax for iterating a fixed number of times,
which looks like this.

```liquid
{% for i in range (1..100) %}
        ...
{% endfor %}
```

Previously, we converted these ranges to arrays using `to_a`, which
initialized an array containing each number in the range. Since all we
use these ranges for is iteration, this is far less efficient than
using a range iterator.

Doing this means that iterating over ranges now takes O(1) rather than O(n)
memory. See the PR for more benchmarks.

* Remove to_a cast on ranges
* Add ReversableRange iterator
* Add custom range-specific slicing logic
2019-07-09 10:57:39 -04:00
Florian Weingarten
b3b63a683f Merge pull request #1097 from ashmaroli/stackprof-no-jruby
Don't attempt to install stackprof gem on JRuby
2019-04-24 09:11:44 -04:00
Ashwin Maroli
1c577c5b62 Don't attempt to install stackprof gem on JRuby 2019-04-24 11:31:20 +05:30
David Cornu
755d2821f3 Merge pull request #1094 from Shopify/for-tag/invalid-limit-offset
Make sure the for tag's limit and offset are integers
2019-04-23 17:20:54 -04:00
David Cornu
495b3d312f Merge pull request #1095 from Shopify/travis/remove-rainbow-gem
Stop installing the rainbow gem on Travis
2019-04-23 17:20:38 -04:00
David Cornu
453f6348c2 Stop installing the rainbow gem on Travis 2019-04-23 16:55:37 -04:00
David Cornu
70ed1fc86d Make sure the limit and offset values are integers 2019-04-23 16:44:37 -04:00
Florian Weingarten
c2ef247be5 Merge pull request #1092 from Shopify/rake-memory-profiler-task
rake memory_profile:run
2019-04-22 16:33:32 -04:00
Florian Weingarten
1518d3f6f9 Merge pull request #1093 from Shopify/bytesize-not-length
use bytesize, not length
2019-04-18 18:39:21 +01:00
Florian Weingarten
c67b77709d rake memory_profile:run 2019-04-17 19:09:26 +01:00
Florian Weingarten
c89ce9c2ed use bytesize, not length 2019-04-17 18:55:13 +01:00
Richard Monette
b0629f17f7 Merge pull request #1073 from Shopify/defer-alloc-hash
defer hash allocation until needed for unparsed_args
2019-03-20 13:34:48 -04:00
Richard Monette
274f078806 defer hash allocation in parse_filter_expressions
add exploration of GC object allocation

remove performance test

can actually remove one more if branch

use named locals to improve readability
2019-03-20 13:20:31 -04:00
Richard Monette
d7171aa084 Merge pull request #1077 from Shopify/update-cops-for-trailing-comma
update Rubocop for trailing comma styles
2019-03-19 16:02:26 -04:00
Richard Monette
06c4789dc5 update Rubocop for trailing comma styles 2019-03-19 11:05:05 -04:00
Justin Li
f2f467bdbc v4.0.3 2019-03-12 12:43:48 -04:00
Justin Li
ff99d92c18 Merge pull request #1072 from Shopify/fix-interrupts
Fix interrupts through includes
2019-03-12 12:26:12 -04:00
Justin Li
39fecd06db Fix interrupts through includes 2019-03-12 12:18:22 -04:00
22 changed files with 428 additions and 53 deletions

View File

@@ -1,4 +1,6 @@
inherit_from: ./.rubocop_todo.yml inherit_from:
- .rubocop_todo.yml
- ./.rubocop_todo.yml
AllCops: AllCops:
Exclude: Exclude:
@@ -29,7 +31,7 @@ Lint/ParenthesesAsGroupedExpression:
Lint/UnusedBlockArgument: Lint/UnusedBlockArgument:
Enabled: false Enabled: false
Lint/EndAlignment: Layout/EndAlignment:
EnforcedStyleAlignWith: variable EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument: Lint/UnusedMethodArgument:
@@ -59,7 +61,7 @@ Style/BracesAroundHashParameters:
Style/NumericLiterals: Style/NumericLiterals:
Enabled: false Enabled: false
Layout/SpaceInsideBrackets: Layout/SpaceInsideArrayLiteralBrackets:
Enabled: false Enabled: false
Layout/SpaceBeforeBlockBraces: Layout/SpaceBeforeBlockBraces:
@@ -71,7 +73,10 @@ Style/Documentation:
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Enabled: false Enabled: false
Style/TrailingCommaInLiteral: Style/TrailingCommaInArrayLiteral:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false Enabled: false
Layout/IndentHash: Layout/IndentHash:
@@ -104,7 +109,7 @@ Style/SymbolLiteral:
Performance/Count: Performance/Count:
Enabled: false Enabled: false
Style/ConstantName: Naming/ConstantName:
Enabled: false Enabled: false
Layout/CaseIndentation: Layout/CaseIndentation:
@@ -122,6 +127,6 @@ Style/TrivialAccessors:
Style/WordArray: Style/WordArray:
Enabled: false Enabled: false
Style/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'

View File

@@ -1,14 +1,22 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2019-02-03 21:12:39 +0530 using RuboCop version 0.49.1. # on 2019-03-19 11:04:37 -0400 using RuboCop version 0.53.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude:
- 'liquid.gemspec'
# Offense count: 5 # Offense count: 5
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent # SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc: Layout/IndentHeredoc:
Exclude: Exclude:
@@ -17,7 +25,7 @@ Layout/IndentHeredoc:
# Offense count: 6 # Offense count: 6
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: symmetrical, new_line, same_line # SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout: Layout/MultilineMethodCallBraceLayout:
Exclude: Exclude:
@@ -26,38 +34,65 @@ Layout/MultilineMethodCallBraceLayout:
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error # SupportedStyles: runtime_error, standard_error
Lint/InheritException: Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 51 # Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
# Offense count: 54
Metrics/AbcSize: Metrics/AbcSize:
Max: 56 Max: 56
# Offense count: 11 # Offense count: 12
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 12 Max: 12
# Offense count: 639 # Offense count: 112
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 108
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/MethodLength: Metrics/MethodLength:
Max: 37 Max: 37
# Offense count: 7 # Offense count: 8
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 11 Max: 11
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude:
- 'test/integration/assign_test.rb'
- 'test/integration/capture_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 23
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 10 # Offense count: 10
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method # SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias: Style/Alias:
Exclude: Exclude:
@@ -69,14 +104,23 @@ Style/Alias:
- 'lib/liquid/tags/include.rb' - 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
# Offense count: 22
Style/CommentedKeyword:
Enabled: false
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions. # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition # SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment: Style/ConditionalAssignment:
Exclude: Exclude:
- 'lib/liquid/errors.rb' - 'lib/liquid/errors.rb'
# Offense count: 1
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/EmptyCaseCondition: Style/EmptyCaseCondition:
@@ -86,7 +130,7 @@ Style/EmptyCaseCondition:
# Offense count: 5 # Offense count: 5
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded # SupportedStyles: compact, expanded
Style/EmptyMethod: Style/EmptyMethod:
Exclude: Exclude:
@@ -96,11 +140,28 @@ Style/EmptyMethod:
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb' - 'test/unit/context_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2 # Offense count: 2
# Configuration parameters: SupportedStyles. # Cop supports --auto-correct.
# SupportedStyles: annotated, template Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken: Style/FormatStringToken:
EnforcedStyle: template Exclude:
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 14 # Offense count: 14
# Configuration parameters: MinBodyLength. # Configuration parameters: MinBodyLength.
@@ -121,7 +182,7 @@ Style/GuardClause:
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. # Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always # SupportedStyles: skip_modifier_ifs, always
Style/Next: Style/Next:
Exclude: Exclude:
@@ -129,7 +190,7 @@ Style/Next:
# Offense count: 4 # Offense count: 4
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles. # Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison # SupportedStyles: predicate, comparison
Style/NumericPredicate: Style/NumericPredicate:
Exclude: Exclude:
@@ -166,14 +227,14 @@ Style/Semicolon:
# Offense count: 7 # Offense count: 7
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MinSize, SupportedStyles. # Configuration parameters: MinSize.
# SupportedStyles: percent, brackets # SupportedStyles: percent, brackets
Style/SymbolArray: Style/SymbolArray:
EnforcedStyle: brackets EnforcedStyle: brackets
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. # Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses: Style/TernaryParentheses:
Exclude: Exclude:
@@ -188,7 +249,12 @@ Style/UnneededPercentQ:
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier: Style/WhileUntilModifier:
Exclude: Exclude:
- 'lib/liquid/tags/case.rb' - 'lib/liquid/tags/case.rb'
# Offense count: 640
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294

View File

@@ -23,7 +23,6 @@ matrix:
- rvm: jruby-head - rvm: jruby-head
install: install:
- gem install rainbow -v 2.2.1
- bundle install - bundle install
script: bundle exec rake script: bundle exec rake

11
Gemfile
View File

@@ -5,16 +5,19 @@ end
gemspec gemspec
gem 'stackprof', platforms: :mri
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
gem 'stackprof'
end
end end
group :test do group :test do
gem 'rubocop', '~> 0.49.0' gem 'rubocop', '~> 0.53.0'
platform :mri do platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'reversable-range'
end end
end end

View File

@@ -1,5 +1,10 @@
# Liquid Change Log # Liquid Change Log
## 4.0.3 / 2019-03-12
### Fixed
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
## 4.0.2 / 2019-03-08 ## 4.0.2 / 2019-03-08
### Changed ### Changed

View File

@@ -85,6 +85,13 @@ namespace :profile do
end end
end end
namespace :memory_profile do
desc "Run memory profiler"
task :run do
ruby "./performance/memory_profile.rb"
end
end
desc "Run example" desc "Run example"
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"

View File

@@ -74,6 +74,7 @@ require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/reversable_range'
# Load all the tags of the standard library # Load all the tags of the standard library
# #

View File

@@ -89,6 +89,7 @@ module Liquid
break break
else # Other non-Block tags else # Other non-Block tags
render_node_to_output(node, output, context) render_node_to_output(node, output, context)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
end end
@@ -114,7 +115,7 @@ module Liquid
end end
def check_resources(context, node_output) def check_resources(context, node_output)
context.resource_limits.render_length += node_output.length context.resource_limits.render_length += node_output.bytesize
return unless context.resource_limits.reached? return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError.new("Memory limits exceeded".freeze)
end end

View File

@@ -6,7 +6,7 @@ module Liquid
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
start_obj.to_i..end_obj.to_i ReversableRange.new(start_obj.to_i, end_obj.to_i)
end end
end end
@@ -18,7 +18,7 @@ module Liquid
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj)) end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int ReversableRange.new(start_int, end_int)
end end
private private

View File

@@ -0,0 +1,77 @@
module Liquid
class ReversableRange
include Enumerable
def initialize(min, max)
@min = min
@max = max
@reversed = false
end
def each
if reversed
index = max
while index >= min
yield index
index -= 1
end
else
index = min
while index <= max
yield index
index += 1
end
end
end
def reverse!
@reversed = !reversed
self
end
def empty?
max < min
end
def size
difference = max - min
if difference > 0
difference + 1
else
0
end
end
def load_slice(from, to = nil)
to ||= max
slice_max = [max, to].min
slice_min = [min, from].max
range = ReversableRange.new(slice_min, slice_max)
range.reverse! if reversed
range
end
def ==(other)
other.is_a?(self.class) &&
other.min == min &&
other.max == max &&
other.reversed == reversed
end
def to_s
if reversed
"#{max}..#{min}"
else
"#{min}..#{max}"
end
end
def to_liquid
self
end
protected
attr_reader :min, :max, :reversed
end
end

View File

@@ -37,7 +37,7 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.length val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash) elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.

View File

@@ -25,7 +25,7 @@ module Liquid
def render(context) def render(context)
output = super output = super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits.assign_score += output.length context.resource_limits.assign_score += output.bytesize
''.freeze ''.freeze
end end

View File

@@ -124,26 +124,34 @@ module Liquid
from = if @from == :continue from = if @from == :continue
offsets[@name].to_i offsets[@name].to_i
else else
context.evaluate(@from).to_i from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit) limit_value = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed segment.reverse! if @reversed
offsets[@name] = from + segment.length offsets[@name] = from + segment.size
segment segment
end end
def render_segment(context, segment) def render_segment(context, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.size
result = '' result = ''

View File

@@ -1,6 +1,8 @@
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
return collection.load_slice(from, to) if collection.is_a?(ReversableRange)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to) collection.load_slice(from, to)
else else

View File

@@ -95,16 +95,17 @@ module Liquid
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = {} keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if matches = a.match(JustTagAttributes) if matches = a.match(JustTagAttributes)
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
result << keyword_args unless keyword_args.empty? result << keyword_args if keyword_args
result result
end end

View File

@@ -1,5 +1,5 @@
# encoding: utf-8 # encoding: utf-8
module Liquid module Liquid
VERSION = "4.0.2".freeze VERSION = "4.0.3".freeze
end end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'benchmark/ips'
require 'memory_profiler'
require_relative 'theme_runner'
def profile(phase, &block)
puts
puts "#{phase}:"
puts
report = MemoryProfiler.report(&block)
report.pretty_print(
color_output: true,
scale_bytes: true,
detailed_report: true
)
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profile("Parsing") { profiler.compile }
profile("Rendering") { profiler.render }

View File

@@ -36,6 +36,10 @@ HERE
assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns) assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns)
end end
def test_for_range_reversed
assert_template_result('321', '{%for item in (1..3) reversed %}{{item}}{%endfor%}', {})
end
def test_for_with_range def test_for_with_range
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}') assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
@@ -103,6 +107,34 @@ HERE
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: true offset: 1 %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP
{% for i in array limit: 1 offset: true %}
{{ i }}
{% endfor %}
MKUP
exception = assert_raises(Liquid::ArgumentError) do
Template.parse(template).render!(assigns)
end
assert_equal("Liquid error: invalid integer", exception.message)
end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2 assigns['limit'] = 2

View File

@@ -30,6 +30,9 @@ class TestFileSystem
when 'assignments' when 'assignments'
"{% assign foo = 'bar' %}" "{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else else
template_path template_path
end end
@@ -242,4 +245,9 @@ class IncludeTagTest < Minitest::Test
assert_equal [], template.errors assert_equal [], template.errors
end end
def test_break_through_include
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}"
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}"
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -139,6 +139,16 @@ class TemplateTest < Minitest::Test
refute_nil t.resource_limits.assign_score refute_nil t.resource_limits.assign_score
end end
def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}")
t.render
assert_equal 9, t.resource_limits.assign_score
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render
assert_equal 9, t.resource_limits.assign_score
end
def test_resource_limits_assign_score_nested def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}") t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
@@ -187,6 +197,14 @@ class TemplateTest < Minitest::Test
assert_equal "ababab", t.render assert_equal "ababab", t.render
end end
def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10
assert_equal "Liquid error: Memory limits exceeded", t.render
t.resource_limits.render_length_limit = 18
assert_equal "すごい", t.render
end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new context = Context.new
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}")

View File

@@ -349,9 +349,9 @@ class ContextUnitTest < Minitest::Test
def test_ranges def test_ranges
@context.merge("test" => '5') @context.merge("test" => '5')
assert_equal (1..5), @context['(1..5)'] assert_equal ReversableRange.new(1, 5), @context['(1..5)']
assert_equal (1..5), @context['(1..test)'] assert_equal ReversableRange.new(1, 5), @context['(1..test)']
assert_equal (5..5), @context['(test..test)'] assert_equal ReversableRange.new(5, 5), @context['(test..test)']
end end
def test_cents_through_drop_nestedly def test_cents_through_drop_nestedly

View File

@@ -0,0 +1,116 @@
require 'test_helper'
class ReversableRangeTest < Minitest::Test
include Liquid
def test_each_iterates_through_items_in_the_range
actual_items = []
ReversableRange.new(1, 10).each { |item| actual_items << item }
expected_items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
assert_equal expected_items, actual_items
end
def test_implements_enumerable
actual_items = ReversableRange.new(1, 10).select(&:even?)
expected_items = [2, 4, 6, 8, 10]
assert_equal expected_items, actual_items
end
def test_is_not_empty_max_greater_than_min
range = ReversableRange.new(9, 10)
refute_predicate range, :empty?
end
def test_is_not_empty_max_equal_to_min
range = ReversableRange.new(10, 10)
refute_predicate range, :empty?
end
def test_is_empty_if_not_reversed_and_max_less_than_min
range = ReversableRange.new(10, 9)
assert_predicate range, :empty?
end
def test_ranges_with_the_same_max_and_min_have_one_element
actual_items = ReversableRange.new(1337, 1337).to_a
expected_items = [1337]
assert_equal expected_items, actual_items
end
def test_load_slice_returns_a_sub_range
actual_items = ReversableRange.new(1, 10).load_slice(5, 8).to_a
expected_items = [5, 6, 7, 8]
assert_equal expected_items, actual_items
end
def test_load_slice_returns_a_reversed_sub_range_if_reversed
range = ReversableRange.new(1, 10)
range.reverse!
actual_items = range.load_slice(5, 8).to_a
expected_items = [8, 7, 6, 5]
assert_equal expected_items, actual_items
end
def test_is_equal_to_other_if_also_a_reversable_range_and_has_same_properties
one = ReversableRange.new(1, 10)
one.reverse!
two = ReversableRange.new(1, 10)
two.reverse!
assert_equal one, two
end
def test_is_not_equal_to_a_non_reversable_range
range = ReversableRange.new(1, 10)
range.reverse!
refute_equal range, :something_else
end
def test_is_not_equal_if_ranges_have_different_mins
one = ReversableRange.new(1, 10)
two = ReversableRange.new(2, 10)
refute_equal one, two
end
def test_is_not_equal_if_ranges_have_different_maxes
one = ReversableRange.new(1, 10)
two = ReversableRange.new(1, 11)
refute_equal one, two
end
def test_is_not_equal_if_only_one_is_reversed
one = ReversableRange.new(1, 10)
two = ReversableRange.new(1, 10)
two.reverse!
refute_equal one, two
end
def test_to_s_mirrors_rubys_range_syntax
range = ReversableRange.new(1, 10)
assert_equal '1..10', range.to_s
end
def test_to_s_reverses_when_reversed
range = ReversableRange.new(1, 10)
range.reverse!
assert_equal '10..1', range.to_s
end
def test_size
range = ReversableRange.new(1, 10)
assert_equal 10, range.size
end
end