Compare commits

..

44 Commits

Author SHA1 Message Date
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
Gaurav Chande
c424d47274 Add changelog entry for Strainer method override change 2016-02-24 20:23:57 +00:00
Gaurav Chande
8e6b9d503d Make Strainer also raise when registered method is overriden as protected 2016-02-24 20:23:49 +00:00
Gaurav Chande
8be38d1795 Strainer#add_filter should raise when registered method is overriden as private 2016-02-24 20:03:17 +00:00
Justin Li
3146d5c3f2 Grammatic and other fixes to CONTRIBUTING.md 2016-02-02 23:45:37 -05:00
Justin Li
0cc8b68a97 Make logic in Context#lookup_and_evaluate more understandable 2016-02-02 23:22:46 -05:00
Justin Li
5a50c12953 Update history to reflect merge of #691
[ci skip]
2016-02-02 23:14:41 -05:00
Justin Li
a6fa4c5c38 Merge pull request #691 from urbandictionary/missing_variables_and_filters
Merge pull request 691
2016-02-02 23:13:44 -05:00
Ivan Kuznetsov
dadd9b4dd2 Add strict_variables/strict_filters render options to check for undefined variables and filters 2016-02-03 10:49:33 +07:00
Justin Li
6434b8d2bb Merge pull request #696 from Shopify/no-send
Remove possibility for arbitrary sends
2016-02-02 11:01:46 -05:00
Justin Li
2d891ddd8f Merge pull request #695 from Shopify/assign-score
Take nested values into account for assign score
2016-02-01 13:14:40 -05:00
Justin Li
60b508b151 Use #each to avoid extra allocations 2016-02-01 13:01:25 -05:00
Justin Li
3891f14a1a Take nested values into account for assign score 2016-02-01 13:01:25 -05:00
Justin Li
198f0aa366 Add test for nested assign score bookkeeping 2016-02-01 13:01:23 -05:00
Justin Li
f2e6adf566 Remove arbitrary send vector 2016-02-01 11:38:40 -05:00
Justin Li
08de6ed2c5 Merge pull request #687 from pathawks/default
Performance improvement: `default` filter
2016-01-24 11:34:05 -05:00
Pat Hawks
7e322f5cf8 Performance improvement: default filter 2016-01-23 23:18:51 -08:00
Justin Li
bf86a5a069 Merge pull request #688 from Shopify/gmp
Install libgmp3-dev in travis
2016-01-23 21:46:37 -05:00
Justin Li
0141444814 Install libgmp3-dev in travis 2016-01-23 21:41:14 -05:00
Justin Li
6d30226768 Update changelog for 4.0.0rc1 2016-01-08 15:08:06 -05:00
31 changed files with 443 additions and 127 deletions

View File

@@ -10,6 +10,11 @@ rvm:
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
allow_failures:
- rvm: jruby-head

View File

@@ -4,23 +4,22 @@
* Bugfixes
* Performance improvements
* Features which are likely to be useful to the majority of Liquid users
* Features that are likely to be useful to the majority of Liquid users
## Things we won't merge
* Code which introduces considerable performance degrations
* Code which touches performance critical parts of Liquid and comes without benchmarks
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code which comes without tests
* Code which breaks existing tests
* Code that introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

View File

@@ -3,6 +3,10 @@
## 4.0.0 / not yet released / branch "master"
### Changed
* Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke]
@@ -15,9 +19,12 @@
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove support for `liquid_methods`
* Rename Drop method `before_method` as `liquid_method_missing` (#661) [Thierry Joyal]
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten]

View File

@@ -73,3 +73,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```

View File

@@ -76,6 +76,9 @@ module Liquid
end
rescue MemoryError => e
raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number)
output << nil
rescue ::StandardError => e
output << context.handle_error(e, token.line_number)
end

View File

@@ -13,7 +13,7 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler, :template_name, :partial, :global_filter
attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@@ -21,6 +21,7 @@ module Liquid
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
@@ -205,7 +206,13 @@ module Liquid
end
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
else
value

View File

@@ -24,8 +24,9 @@ module Liquid
attr_writer :context
# Catch all for the method
def liquid_method_missing(_method)
nil
def liquid_method_missing(method)
return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
# called by liquid to invoke a drop
@@ -61,16 +62,17 @@ module Liquid
end
def self.invokable_methods
unless @invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s))
Set.new(whitelist.map(&:to_s))
end
@invokable_methods
end
end
end

View File

@@ -56,4 +56,8 @@ module Liquid
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
end

View File

@@ -25,6 +25,12 @@ class Numeric # :nodoc:
end
end
class Range # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
def to_liquid
self

View File

@@ -65,7 +65,7 @@ module Liquid
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path
end

View File

@@ -1,7 +1,7 @@
module Liquid
class ParseContext
attr_accessor :partial, :locale, :line_number
attr_reader :warnings, :error_mode
attr_accessor :locale, :line_number
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}

View File

@@ -125,8 +125,6 @@ module Liquid
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property) <=> b.send(property) }
end
end
@@ -141,8 +139,6 @@ module Liquid
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort { |a, b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
end
end
@@ -191,8 +187,6 @@ module Liquid
[]
elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? }
elsif ary.first.respond_to?(property)
ary.reject { |a| a.send(property).nil? }
end
end
@@ -222,6 +216,9 @@ module Liquid
end
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
end
InputIterator.new(input).concat(array)
end
@@ -292,6 +289,12 @@ module Liquid
array.last if array.respond_to?(:last)
end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
@@ -341,9 +344,12 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty?
default_value
else
input
end
end
private
@@ -373,7 +379,7 @@ module Liquid
end
def concat(args)
to_a.concat args
to_a.concat(args)
end
def reverse

View File

@@ -26,10 +26,15 @@ module Liquid
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.class.include?(filter)
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
@@ -48,6 +53,8 @@ module Liquid
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end

View File

@@ -23,16 +23,28 @@ module Liquid
def render(context)
val = @from.render(context)
context.scopes.last[@to] = val
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
context.resource_limits.assign_score += assign_score_of(val)
''.freeze
end
def blank?
true
end
private
def assign_score_of(val)
if val.instance_of?(String)
val.length
elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
sum
else
1
end
end
end
Template.register_tag('assign'.freeze, Assign)

View File

@@ -48,8 +48,10 @@ module Liquid
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@else_block = nil
end
def parse(tokens)

View File

@@ -6,9 +6,7 @@ module Liquid
def initialize(tag_name, markup, parse_context)
super
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
ensure_valid_markup(tag_name, markup, parse_context)
end
def parse(tokens)
@@ -35,6 +33,14 @@ module Liquid
def blank?
@body.empty?
end
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end
end
Template.register_tag('raw'.freeze, Raw)

View File

@@ -19,8 +19,10 @@ module Liquid
@@file_system = BlankFileSystem.new
class TagRegistry
include Enumerable
def initialize
@tags = {}
@tags = {}
@cache = {}
end
@@ -41,6 +43,10 @@ module Liquid
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
@@ -80,11 +86,11 @@ module Liquid
end
def error_mode
@error_mode || :lax
@error_mode ||= :lax
end
def taint_mode
@taint_mode || :lax
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available
@@ -107,6 +113,7 @@ module Liquid
end
def initialize
@rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end
@@ -181,12 +188,7 @@ module Liquid
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_handler = options[:exception_handler] if options[:exception_handler]
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
@@ -235,5 +237,13 @@ module Liquid
yield
end
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_handler = options[:exception_handler] if options[:exception_handler]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end

View File

@@ -4,7 +4,7 @@ module Liquid
def initialize(source, line_numbers = false)
@source = source
@line_number = 1 if line_numbers
@line_number = line_numbers ? 1 : nil
@tokens = tokenize
end

View File

@@ -50,9 +50,13 @@ module Liquid
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end

View File

@@ -55,9 +55,11 @@ module Liquid
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to

View File

@@ -1,4 +1,4 @@
# encoding: utf-8
module Liquid
VERSION = "4.0.0.rc1"
VERSION = "4.0.0.rc2"
end

View File

@@ -94,7 +94,7 @@ class ErrorHandlingTest < Minitest::Test
)
end
assert_match /Liquid syntax error \(line 4\)/, err.message
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors

View File

@@ -1,18 +1,18 @@
require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
end
end
include Liquid
def test_global_register_order

View File

@@ -9,6 +9,10 @@ end
class SecurityTest < Minitest::Test
include Liquid
def setup
@assigns = {}
end
def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )

View File

@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
end
end
class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
end
def to_number
@amount
end
end
class StandardFiltersTest < Minitest::Test
include Liquid
@@ -364,20 +374,38 @@ class StandardFiltersTest < Minitest::Test
def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
end
def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
end
def test_abs
assert_template_result "17", "{{ 17 | abs }}"
assert_template_result "17", "{{ -17 | abs }}"
assert_template_result "17", "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}"
assert_template_result "0", "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}"
assert_template_result "17.42", "{{ -17.42 | abs }}"
assert_template_result "17.42", "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}"
end
def test_times
assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
end
def test_divided_by
@@ -391,6 +419,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
end
def test_modulo
@@ -398,6 +428,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
end
def test_round
@@ -407,6 +439,9 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
end
def test_ceil
@@ -415,6 +450,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
end
def test_floor
@@ -423,6 +460,8 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
end
def test_append
@@ -436,8 +475,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_raises(TypeError) do
# no implicit conversion of Fixnum into Array
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
end
end

View File

@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
end
def test_invalid_raw
assert_match_syntax_error /tag was never closed/, '{% raw %} foo'
assert_match_syntax_error /Valid syntax/, '{% raw } foo {% endraw %}'
assert_match_syntax_error /Valid syntax/, '{% raw } foo %}{% endraw %}'
assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
end
end

View File

@@ -27,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
end
end
class DropWithUndefinedMethod < Liquid::Drop
def foo
'foo'
end
end
class TemplateTest < Minitest::Test
include Liquid
@@ -133,6 +139,17 @@ class TemplateTest < Minitest::Test
refute_nil t.resource_limits.assign_score
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end
def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50
@@ -225,4 +242,78 @@ class TemplateTest < Minitest::Test
assert_equal 'BOB filtered', rendered_template
end
def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
assert_equal '33 32 ', result
assert_equal 3, t.errors.count
assert_instance_of Liquid::UndefinedVariable, t.errors[0]
assert_equal 'Liquid error: undefined variable y', t.errors[0].message
assert_instance_of Liquid::UndefinedVariable, t.errors[1]
assert_equal 'Liquid error: undefined variable b', t.errors[1].message
assert_instance_of Liquid::UndefinedVariable, t.errors[2]
assert_equal 'Liquid error: undefined variable d', t.errors[2].message
end
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
end
end
def test_undefined_drop_methods
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, { strict_variables: true })
assert_equal 'foo ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
end
def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
t.render!(d, { strict_variables: true })
end
end
def test_undefined_filters
t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
filters = Module.new do
def somefilter3(v)
"-#{v}-"
end
end
result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
assert_equal '123 ', result
assert_equal 1, t.errors.count
assert_instance_of Liquid::UndefinedFilter, t.errors[0]
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
end
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
t.render!({ 'x' => 'foo' }, { strict_filters: true })
end
end
def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '1..5', result
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render({ 'x' => 1, 'y' => 5 })
assert_equal '12345', result
end
end

View File

@@ -46,6 +46,8 @@ class BlockUnitTest < Minitest::Test
def test_with_custom_tag
Liquid::Template.register_tag("testtag", Block)
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
ensure
Liquid::Template.tags.delete('testtag')
end
private

View File

@@ -3,50 +3,54 @@ require 'test_helper'
class ConditionUnitTest < Minitest::Test
include Liquid
def setup
@context = Liquid::Context.new
end
def test_basic_condition
assert_equal false, Condition.new(1, '==', 2).evaluate
assert_equal true, Condition.new(1, '==', 1).evaluate
end
def test_default_operators_evalute_true
assert_evalutes_true 1, '==', 1
assert_evalutes_true 1, '!=', 2
assert_evalutes_true 1, '<>', 2
assert_evalutes_true 1, '<', 2
assert_evalutes_true 2, '>', 1
assert_evalutes_true 1, '>=', 1
assert_evalutes_true 2, '>=', 1
assert_evalutes_true 1, '<=', 2
assert_evalutes_true 1, '<=', 1
assert_evaluates_true 1, '==', 1
assert_evaluates_true 1, '!=', 2
assert_evaluates_true 1, '<>', 2
assert_evaluates_true 1, '<', 2
assert_evaluates_true 2, '>', 1
assert_evaluates_true 1, '>=', 1
assert_evaluates_true 2, '>=', 1
assert_evaluates_true 1, '<=', 2
assert_evaluates_true 1, '<=', 1
# negative numbers
assert_evalutes_true 1, '>', -1
assert_evalutes_true -1, '<', 1
assert_evalutes_true 1.0, '>', -1.0
assert_evalutes_true -1.0, '<', 1.0
assert_evaluates_true 1, '>', -1
assert_evaluates_true (-1), '<', 1
assert_evaluates_true 1.0, '>', -1.0
assert_evaluates_true (-1.0), '<', 1.0
end
def test_default_operators_evalute_false
assert_evalutes_false 1, '==', 2
assert_evalutes_false 1, '!=', 1
assert_evalutes_false 1, '<>', 1
assert_evalutes_false 1, '<', 0
assert_evalutes_false 2, '>', 4
assert_evalutes_false 1, '>=', 3
assert_evalutes_false 2, '>=', 4
assert_evalutes_false 1, '<=', 0
assert_evalutes_false 1, '<=', 0
assert_evaluates_false 1, '==', 2
assert_evaluates_false 1, '!=', 1
assert_evaluates_false 1, '<>', 1
assert_evaluates_false 1, '<', 0
assert_evaluates_false 2, '>', 4
assert_evaluates_false 1, '>=', 3
assert_evaluates_false 2, '>=', 4
assert_evaluates_false 1, '<=', 0
assert_evaluates_false 1, '<=', 0
end
def test_contains_works_on_strings
assert_evalutes_true 'bob', 'contains', 'o'
assert_evalutes_true 'bob', 'contains', 'b'
assert_evalutes_true 'bob', 'contains', 'bo'
assert_evalutes_true 'bob', 'contains', 'ob'
assert_evalutes_true 'bob', 'contains', 'bob'
assert_evaluates_true 'bob', 'contains', 'o'
assert_evaluates_true 'bob', 'contains', 'b'
assert_evaluates_true 'bob', 'contains', 'bo'
assert_evaluates_true 'bob', 'contains', 'ob'
assert_evaluates_true 'bob', 'contains', 'bob'
assert_evalutes_false 'bob', 'contains', 'bob2'
assert_evalutes_false 'bob', 'contains', 'a'
assert_evalutes_false 'bob', 'contains', '---'
assert_evaluates_false 'bob', 'contains', 'bob2'
assert_evaluates_false 'bob', 'contains', 'a'
assert_evaluates_false 'bob', 'contains', '---'
end
def test_invalid_comparation_operator
@@ -65,29 +69,29 @@ class ConditionUnitTest < Minitest::Test
@context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array")
assert_evalutes_false array_expr, 'contains', 0
assert_evalutes_true array_expr, 'contains', 1
assert_evalutes_true array_expr, 'contains', 2
assert_evalutes_true array_expr, 'contains', 3
assert_evalutes_true array_expr, 'contains', 4
assert_evalutes_true array_expr, 'contains', 5
assert_evalutes_false array_expr, 'contains', 6
assert_evalutes_false array_expr, 'contains', "1"
assert_evaluates_false array_expr, 'contains', 0
assert_evaluates_true array_expr, 'contains', 1
assert_evaluates_true array_expr, 'contains', 2
assert_evaluates_true array_expr, 'contains', 3
assert_evaluates_true array_expr, 'contains', 4
assert_evaluates_true array_expr, 'contains', 5
assert_evaluates_false array_expr, 'contains', 6
assert_evaluates_false array_expr, 'contains', "1"
end
def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new
assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
end
def test_contains_return_false_on_wrong_data_type
assert_evalutes_false 1, 'contains', 0
assert_evaluates_false 1, 'contains', 0
end
def test_contains_with_string_left_operand_coerces_right_operand_to_string
assert_evalutes_true ' 1 ', 'contains', 1
assert_evalutes_false ' 1 ', 'contains', 2
assert_evaluates_true ' 1 ', 'contains', 1
assert_evaluates_false ' 1 ', 'contains', 2
end
def test_or_condition
@@ -121,8 +125,8 @@ class ConditionUnitTest < Minitest::Test
def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
assert_evalutes_true 'bob', 'starts_with', 'b'
assert_evalutes_false 'bob', 'starts_with', 'o'
assert_evaluates_true 'bob', 'starts_with', 'b'
assert_evaluates_false 'bob', 'starts_with', 'o'
ensure
Condition.operators.delete 'starts_with'
end
@@ -131,24 +135,24 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
end
private
def assert_evalutes_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
def assert_evaluates_true(left, op, right)
assert Condition.new(left, op, right).evaluate(@context),
"Evaluated false: #{left} #{op} #{right}"
end
def assert_evalutes_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
def assert_evaluates_false(left, op, right)
assert !Condition.new(left, op, right).evaluate(@context),
"Evaluated true: #{left} #{op} #{right}"
end
def assert_evaluates_argument_error(left, op, right)
assert_raises(Liquid::ArgumentError) do
Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
Condition.new(left, op, right).evaluate(@context)
end
end
end # ConditionTest

View File

@@ -77,4 +77,60 @@ class StrainerUnitTest < Minitest::Test
assert_kind_of b, strainer
assert_kind_of Liquid::StandardFilters, strainer
end
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
assert_raises ArgumentError do
s.class.add_filter(wrong_filter)
end
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert strainer.class.filter_methods.include?('public_filter')
end
end # StrainerTest

View File

@@ -67,4 +67,12 @@ class TemplateUnitTest < Minitest::Test
Template.tags.delete('fake')
assert_nil Template.tags['fake']
end
def test_tags_can_be_looped_over
Template.register_tag('fake', FakeTag)
result = Template.tags.map { |name, klass| [name, klass] }
assert result.include?(["fake", "TemplateUnitTest::FakeTag"])
ensure
Template.tags.delete('fake')
end
end