diff --git a/lib/puppet/functions/each.rb b/lib/puppet/functions/each.rb new file mode 100644 index 000000000..4bee461e1 --- /dev/null +++ b/lib/puppet/functions/each.rb @@ -0,0 +1,90 @@ +# Applies a parameterized block to each element in a sequence of selected entries from the first +# argument and returns the first argument. +# +# This function takes two mandatory arguments: the first should be an Array or a Hash or something that is +# of enumerable type (integer, Integer range, or String), and the second +# a parameterized block as produced by the puppet syntax: +# +# $a.each |$x| { ... } +# each($a) |$x| { ... } +# +# When the first argument is an Array (or of enumerable type other than Hash), the parameterized block +# should define one or two block parameters. +# For each application of the block, the next element from the array is selected, and it is passed to +# the block if the block has one parameter. If the block has two parameters, the first is the elements +# index, and the second the value. The index starts from 0. +# +# $a.each |$index, $value| { ... } +# each($a) |$index, $value| { ... } +# +# When the first argument is a Hash, the parameterized block should define one or two parameters. +# When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, +# and when two parameters are defined the iteration is performed with key and value. +# +# $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } +# $a.each |$key, $value| { ..."key ${key}, value ${value}" } +# +# @example using each +# +# [1,2,3].each |$val| { ... } # 1, 2, 3 +# [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) +# {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] +# {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) +# Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... +# "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' +# 3.each |$number| { ... } # 0, 1, 2 +# +# @since 3.2 for Array and Hash +# @since 3.5 for other enumerables +# @note requires `parser = future` +# +Puppet::Functions.create_function(:each) do + dispatch :foreach_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :foreach_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def foreach_Hash(hash, pblock) + enumerator = hash.each_pair + if asserted_serving_size(pblock, 'key') == 1 + (hash.size).times do + pblock.call(nil, enumerator.next) + end + else + (hash.size).times do + pblock.call(nil, *enumerator.next) + end + end + # produces the receiver + hash + end + + def foreach_Enumerable(enumerable, pblock) + enum = asserted_enumerable(enumerable) + index = 0 + if asserted_serving_size(pblock, 'index') == 1 + begin + loop { pblock.call(nil, enum.next) } + rescue StopIteration + end + else + begin + loop do + pblock.call(nil, index, enum.next) + index = index +1 + end + rescue StopIteration + end + end + # produces the receiver + enumerable + end +end diff --git a/lib/puppet/functions/filter.rb b/lib/puppet/functions/filter.rb new file mode 100644 index 000000000..e5610ca86 --- /dev/null +++ b/lib/puppet/functions/filter.rb @@ -0,0 +1,84 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument and returns an array or hash (same type as left operand for array/hash, and array for +# other enumerable types) with the entries for which the block evaluates to `true`. +# +# This function takes two mandatory arguments: the first should be an Array, a Hash, or an +# Enumerable object (integer, Integer range, or String), +# and the second a parameterized block as produced by the puppet syntax: +# +# $a.filter |$x| { ... } +# filter($a) |$x| { ... } +# +# When the first argument is something other than a Hash, the block is called with each entry in turn. +# When the first argument is a Hash the entry is an array with `[key, value]`. +# +# @example Using filter with one parameter +# +# # selects all that end with berry +# $a = ["raspberry", "blueberry", "orange"] +# $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry +# +# If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all +# enumerables except Hash, and to `key, value` for a Hash. +# +# @example Using filter with two parameters +# +# # selects all that end with 'berry' at an even numbered index +# $a = ["raspberry", "blueberry", "orange"] +# $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry +# +# # selects all that end with 'berry' and value >= 1 +# $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} +# $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry +# +# @since 3.4 for Array and Hash +# @since 3.5 for other enumerables +# @note requires `parser = future` +# +Puppet::Functions.create_function(:filter) do + dispatch :filter_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :filter_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def filter_Hash(hash, pblock) + if asserted_serving_size(pblock, 'key') == 1 + result = hash.select {|x, y| pblock.call(self, [x, y]) } + else + result = hash.select {|x, y| pblock.call(self, x, y) } + end + # Ruby 1.8.7 returns Array + result = Hash[result] unless result.is_a? Hash + result + end + + def filter_Enumerable(enumerable, pblock) + result = [] + index = 0 + enum = asserted_enumerable(enumerable) + + if asserted_serving_size(pblock, 'index') == 1 + begin + loop { pblock.call(nil, it = enum.next) == true ? result << it : nil } + rescue StopIteration + end + else + begin + loop do + pblock.call(nil, index, it = enum.next) == true ? result << it : nil + index = index +1 + end + rescue StopIteration + end + end + result + end +end diff --git a/lib/puppet/functions/map.rb b/lib/puppet/functions/map.rb new file mode 100644 index 000000000..1e224e141 --- /dev/null +++ b/lib/puppet/functions/map.rb @@ -0,0 +1,78 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument and returns an array with the result of each invocation of the parameterized block. +# +# This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type +# (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: +# +# $a.map |$x| { ... } +# map($a) |$x| { ... } +# +# When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. +# When the first argument is a hash the entry is an array with `[key, value]`. +# +# @example Using map with two arguments +# +# # Turns hash into array of values +# $a.map |$x|{ $x[1] } +# +# # Turns hash into array of keys +# $a.map |$x| { $x[0] } +# +# When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash +# is given to the block's first parameter, and the value is given to the block's second parameter.args. +# +# @example Using map with two arguments +# +# # Turns hash into array of values +# $a.map |$key,$val|{ $val } +# +# # Turns hash into array of keys +# $a.map |$key,$val|{ $key } +# +# @since 3.4 for Array and Hash +# @since 3.5 for other enumerables, and support for blocks with 2 parameters +# @note requires `parser = future` +# +Puppet::Functions.create_function(:map) do + dispatch :map_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :map_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def map_Hash(hash, pblock) + if asserted_serving_size(pblock, 'key') == 1 + hash.map {|x, y| pblock.call(nil, [x, y]) } + else + hash.map {|x, y| pblock.call(nil, x, y) } + end + end + + def map_Enumerable(enumerable, pblock) + result = [] + index = 0 + enum = asserted_enumerable(enumerable) + if asserted_serving_size(pblock, 'index') == 1 + begin + loop { result << pblock.call(nil, enum.next) } + rescue StopIteration + end + else + begin + loop do + result << pblock.call(nil, index, enum.next) + index = index +1 + end + rescue StopIteration + end + end + result + end +end diff --git a/lib/puppet/functions/reduce.rb b/lib/puppet/functions/reduce.rb new file mode 100644 index 000000000..83850dfe8 --- /dev/null +++ b/lib/puppet/functions/reduce.rb @@ -0,0 +1,102 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. +# +# This function takes two mandatory arguments: the first should be an Array, Hash, or something of +# enumerable type, and the last a parameterized block as produced by the puppet syntax: +# +# $a.reduce |$memo, $x| { ... } +# reduce($a) |$memo, $x| { ... } +# +# When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. +# When the first argument is a hash each entry is converted to an array with `[key, value]` before being +# fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash +# and mandatory block. +# +# $a.reduce(start) |$memo, $x| { ... } +# reduce($a, start) |$memo, $x| { ... } +# +# If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second +# elements of the enumeration, and if the enumerable has fewer than 2 elements, the first +# element is produced as the result of the reduction without invocation of the block. +# +# On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the +# next invocation. +# +# @example Using reduce +# +# # Reduce an array +# $a = [1,2,3] +# $a.reduce |$memo, $entry| { $memo + $entry } +# #=> 6 +# +# # Reduce hash values +# $a = {a => 1, b => 2, c => 3} +# $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } +# #=> [sum, 6] +# +# # reverse a string +# "abc".reduce |$memo, $char| { "$char$memo" } +# #=>"cbe" +# +# It is possible to provide a starting 'memo' as an argument. +# +# @example Using reduce with given start 'memo' +# +# # Reduce an array +# $a = [1,2,3] +# $a.reduce(4) |$memo, $entry| { $memo + $entry } +# #=> 10 +# +# # Reduce hash values +# $a = {a => 1, b => 2, c => 3} +# $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } +# #=> [sum, 10] +# +# @example Using reduce with an Integer range +# +# Integer[1,4].reduce |$memo, $x| { $memo + $x } +# #=> 10 +# +# @since 3.2 for Array and Hash +# @since 3.5 for additional enumerable types +# @note requires `parser = future`. +# +Puppet::Functions.create_function(:reduce) do + + dispatch :reduce_without_memo do + param 'Object', :enumerable + required_block_param + end + + dispatch :reduce_with_memo do + param 'Object', :enumerable + param 'Object', :memo + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def reduce_without_memo(enumerable, pblock) + assert_serving_size(pblock) + enum = asserted_enumerable(enumerable) + enum.reduce {|memo, x| pblock.call(nil, memo, x) } + end + + def reduce_with_memo(enumerable, given_memo, pblock) + assert_serving_size(pblock) + enum = asserted_enumerable(enumerable) + enum.reduce(given_memo) {|memo, x| pblock.call(nil, memo, x) } + end + + # Asserts number of lambda parameters with more specific error message than the generic + # mis-matched arguments message that is produced by the dispatcher's type checking. + # + def assert_serving_size(pblock) + serving_size = pblock.parameter_count + if serving_size != 2 + raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') + end + end +end diff --git a/lib/puppet/functions/slice.rb b/lib/puppet/functions/slice.rb new file mode 100644 index 000000000..e0fe8aaba --- /dev/null +++ b/lib/puppet/functions/slice.rb @@ -0,0 +1,121 @@ +# Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first +# argument and returns the first argument, or if no block is given returns a new array with a concatenation of +# the slices. +# +# This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of +# enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include +# in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: +# +# $a.slice($n) |$x| { ... } +# slice($a) |$x| { ... } +# +# The parameterized block should have either one parameter (receiving an array with the slice), or the same number +# of parameters as specified by the slice size (each parameter receiving its part of the slice). +# In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining +# elements. When the block has multiple parameters, excess parameters are set to :undef for an array or +# enumerable type, and to empty arrays for a Hash. +# +# $a.slice(2) |$first, $second| { ... } +# +# When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce +# an array of two arrays with key, and value. +# +# @example Using slice with Hash +# +# $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } +# $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } +# +# When called without a block, the function produces a concatenated result of the slices. +# +# @example Using slice without a block +# +# slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] +# slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] +# slice(4,2) # produces [[0,1], [2,3]] +# slice('hello',2) # produces [[h, e], [l, l], [o]] +# +# @since 3.2 for Array and Hash +# @since 3.5 for additional enumerable types +# @note requires `parser = future`. +# +Puppet::Functions.create_function(:slice) do + dispatch :slice_Hash do + param 'Hash[Object, Object]', :hash + param 'Integer[1, default]', :slize_size + optional_block_param + end + + dispatch :slice_Enumerable do + param 'Object', :enumerable + param 'Integer[1, default]', :slize_size + optional_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def slice_Hash(hash, slice_size, pblock = nil) + result = slice_Common(hash, slice_size, [], pblock) + pblock ? hash : result + end + + def slice_Enumerable(enumerable, slice_size, pblock = nil) + enum = asserted_enumerable(enumerable) + result = slice_Common(enum, slice_size, :undef, pblock) + pblock ? enumerable : result + end + + def slice_Common(o, slice_size, filler, pblock) + serving_size = asserted_slice_serving_size(pblock, slice_size) + + enumerator = o.each_slice(slice_size) + result = [] + if serving_size == 1 + begin + if pblock + loop do + pblock.call(nil, enumerator.next) + end + else + loop do + result << enumerator.next + end + end + rescue StopIteration + end + else + begin + loop do + a = enumerator.next + if a.size < serving_size + a = a.dup.fill(filler, a.length...serving_size) + end + pblock.call(nil, *a) + end + rescue StopIteration + end + end + if pblock + o + else + result + end + end + + def asserted_slice_serving_size(pblock, slice_size) + if pblock + serving_size = pblock.captures_rest? ? slice_size : pblock.parameter_count + else + serving_size = 1 + end + if serving_size == 0 + raise ArgumentError, "slice(): block must define at least one parameter. Block has 0." + end + unless serving_size == 1 || serving_size == slice_size + raise ArgumentError, "slice(): block must define one parameter, or " + + "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') + end + serving_size + end +end diff --git a/lib/puppet/parser/functions/each.rb b/lib/puppet/parser/functions/each.rb deleted file mode 100644 index 6c8b6356f..000000000 --- a/lib/puppet/parser/functions/each.rb +++ /dev/null @@ -1,110 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:each, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of selected entries from the first - argument and returns the first argument. - - This function takes two mandatory arguments: the first should be an Array or a Hash or something that is - of enumerable type (integer, Integer range, or String), and the second - a parameterized block as produced by the puppet syntax: - - $a.each |$x| { ... } - each($a) |$x| { ... } - - When the first argument is an Array (or of enumerable type other than Hash), the parameterized block - should define one or two block parameters. - For each application of the block, the next element from the array is selected, and it is passed to - the block if the block has one parameter. If the block has two parameters, the first is the elements - index, and the second the value. The index starts from 0. - - $a.each |$index, $value| { ... } - each($a) |$index, $value| { ... } - - When the first argument is a Hash, the parameterized block should define one or two parameters. - When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, - and when two parameters are defined the iteration is performed with key and value. - - $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } - $a.each |$key, $value| { ..."key ${key}, value ${value}" } - - *Examples* - - [1,2,3].each |$val| { ... } # 1, 2, 3 - [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) - {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] - {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) - Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... - "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' - 3.each |$number| { ... } # 0, 1, 2 - - - Since 3.2 for Array and Hash - - Since 3.5 for other enumerables - - requires `parser = future`. - ENDHEREDOC - require 'puppet/parser/ast/lambda' - - def foreach_Hash(o, scope, pblock, serving_size) - enumerator = o.each_pair - if serving_size == 1 - (o.size).times do - pblock.call(scope, enumerator.next) - end - else - (o.size).times do - pblock.call(scope, *enumerator.next) - end - end - end - - def foreach_Enumerator(enumerator, scope, pblock, serving_size) - index = 0 - if serving_size == 1 - begin - loop { pblock.call(scope, enumerator.next) } - rescue StopIteration - end - else - begin - loop do - pblock.call(scope, index, enumerator.next) - index = index +1 - end - rescue StopIteration - end - end - end - - raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; expected 2, got #{args.length})") if args.length != 2 - receiver = args[0] - pblock = args[1] - raise ArgumentError, ("each(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - if serving_size == 0 - raise ArgumentError, "each(): block must define at least one parameter; value. Block has 0." - end - - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - foreach_Hash(receiver, self, pblock, serving_size) - else - if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("each(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - foreach_Enumerator(enum, self, pblock, serving_size) - end - # each always produces the receiver - receiver -end diff --git a/lib/puppet/parser/functions/filter.rb b/lib/puppet/parser/functions/filter.rb deleted file mode 100644 index 17c3f132c..000000000 --- a/lib/puppet/parser/functions/filter.rb +++ /dev/null @@ -1,102 +0,0 @@ -require 'puppet/parser/ast/lambda' - -Puppet::Parser::Functions::newfunction( -:filter, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array or hash (same type as left operand for array/hash, and array for - other enumerable types) with the entries for which the block evaluates to `true`. - - This function takes two mandatory arguments: the first should be an Array, a Hash, or an - Enumerable object (integer, Integer range, or String), - and the second a parameterized block as produced by the puppet syntax: - - $a.filter |$x| { ... } - filter($a) |$x| { ... } - - When the first argument is something other than a Hash, the block is called with each entry in turn. - When the first argument is a Hash the entry is an array with `[key, value]`. - - *Examples* - - # selects all that end with berry - $a = ["raspberry", "blueberry", "orange"] - $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry - - If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all - enumerables except Hash, and to `key, value` for a Hash. - - *Examples* - - # selects all that end with 'berry' at an even numbered index - $a = ["raspberry", "blueberry", "orange"] - $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry - - # selects all that end with 'berry' and value >= 1 - $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} - $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry - - - Since 3.4 for Array and Hash - - Since 3.5 for other enumerables - - requires `parser = future` - ENDHEREDOC - - def filter_Enumerator(enumerator, scope, pblock, serving_size) - result = [] - index = 0 - if serving_size == 1 - begin - loop { pblock.call(scope, it = enumerator.next) == true ? result << it : nil } - rescue StopIteration - end - else - begin - loop do - pblock.call(scope, index, it = enumerator.next) == true ? result << it : nil - index = index +1 - end - rescue StopIteration - end - end - result - end - - receiver = args[0] - pblock = args[1] - - raise ArgumentError, ("filter(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - if serving_size == 0 - raise ArgumentError, "filter(): block must define at least one parameter; value. Block has 0." - end - - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - if serving_size == 1 - result = receiver.select {|x, y| pblock.call(self, [x, y]) } - else - result = receiver.select {|x, y| pblock.call(self, x, y) } - end - # Ruby 1.8.7 returns Array - result = Hash[result] unless result.is_a? Hash - result - else - if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - filter_Enumerator(enum, self, pblock, serving_size) - end -end diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb deleted file mode 100644 index fb10c7177..000000000 --- a/lib/puppet/parser/functions/map.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'puppet/parser/ast/lambda' - -Puppet::Parser::Functions::newfunction( -:map, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array with the result of each invocation of the parameterized block. - - This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type - (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: - - $a.map |$x| { ... } - map($a) |$x| { ... } - - When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. - When the first argument is a hash the entry is an array with `[key, value]`. - - *Examples* - - # Turns hash into array of values - $a.map |$x|{ $x[1] } - - # Turns hash into array of keys - $a.map |$x| { $x[0] } - - When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash - is given to the block's first parameter, and the value is given to the block's second parameter.args. - - *Examples* - - # Turns hash into array of values - $a.map |$key,$val|{ $val } - - # Turns hash into array of keys - $a.map |$key,$val|{ $key } - - - Since 3.4 for Array and Hash - - Since 3.5 for other enumerables, and support for blocks with 2 parameters - - requires `parser = future` - ENDHEREDOC - - def map_Enumerator(enumerator, scope, pblock, serving_size) - result = [] - index = 0 - if serving_size == 1 - begin - loop { result << pblock.call(scope, enumerator.next) } - rescue StopIteration - end - else - begin - loop do - result << pblock.call(scope, index, enumerator.next) - index = index +1 - end - rescue StopIteration - end - end - result - end - - receiver = args[0] - pblock = args[1] - raise ArgumentError, ("map(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - - if serving_size == 0 - raise ArgumentError, "map(): block must define at least one parameter; value. Block has 0." - end - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; key, value.args Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - if serving_size == 1 - result = receiver.map {|x, y| pblock.call(self, [x, y]) } - else - result = receiver.map {|x, y| pblock.call(self, x, y) } - end - else - if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - result = map_Enumerator(enum, self, pblock, serving_size) - end - result -end diff --git a/lib/puppet/parser/functions/reduce.rb b/lib/puppet/parser/functions/reduce.rb deleted file mode 100644 index 3686bdb03..000000000 --- a/lib/puppet/parser/functions/reduce.rb +++ /dev/null @@ -1,102 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:reduce, -:type => :rvalue, -:arity => -2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. - - This function takes two mandatory arguments: the first should be an Array, Hash, or something of - enumerable type, and the last a parameterized block as produced by the puppet syntax: - - $a.reduce |$memo, $x| { ... } - reduce($a) |$memo, $x| { ... } - - When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. - When the first argument is a hash each entry is converted to an array with `[key, value]` before being - fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash - and mandatory block. - - $a.reduce(start) |$memo, $x| { ... } - reduce($a, start) |$memo, $x| { ... } - - If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second - elements of the enumeration, and if the enumerable has fewer than 2 elements, the first - element is produced as the result of the reduction without invocation of the block. - - On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the - next invocation. - - *Examples* - - # Reduce an array - $a = [1,2,3] - $a.reduce |$memo, $entry| { $memo + $entry } - #=> 6 - - # Reduce hash values - $a = {a => 1, b => 2, c => 3} - $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } - #=> [sum, 6] - - # reverse a string - "abc".reduce |$memo, $char| { "$char$memo" } - #=>"cbe" - - It is possible to provide a starting 'memo' as an argument. - - *Examples* - - # Reduce an array - $a = [1,2,3] - $a.reduce(4) |$memo, $entry| { $memo + $entry } - #=> 10 - - # Reduce hash values - $a = {a => 1, b => 2, c => 3} - $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } - #=> [sum, 10] - - *Examples* - - Integer[1,4].reduce |$memo, $x| { $memo + $x } - #=> 10 - - - Since 3.2 for Array and Hash - - Since 3.5 for additional enumerable types - - requires `parser = future`. - ENDHEREDOC - - require 'puppet/parser/ast/lambda' - - case args.length - when 2 - pblock = args[1] - when 3 - pblock = args[2] - else - raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; expected 2 or 3, got #{args.length})") - end - unless pblock.respond_to?(:puppet_lambda) - raise ArgumentError, ("reduce(): wrong argument type (#{pblock.class}; must be a parameterized block.") - end - receiver = args[0] - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("reduce(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - - if serving_size != 2 - raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - - if args.length == 3 - enum.reduce(args[1]) {|memo, x| pblock.call(self, memo, x) } - else - enum.reduce {|memo, x| pblock.call(self, memo, x) } - end -end diff --git a/lib/puppet/parser/functions/slice.rb b/lib/puppet/parser/functions/slice.rb deleted file mode 100644 index b50726d3f..000000000 --- a/lib/puppet/parser/functions/slice.rb +++ /dev/null @@ -1,120 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:slice, -:type => :rvalue, -:arity => -2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first - argument and returns the first argument, or if no block is given returns a new array with a concatenation of - the slices. - - This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of - enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include - in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: - - $a.slice($n) |$x| { ... } - slice($a) |$x| { ... } - - The parameterized block should have either one parameter (receiving an array with the slice), or the same number - of parameters as specified by the slice size (each parameter receiving its part of the slice). - In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining - elements. When the block has multiple parameters, excess parameters are set to :undef for an array or - enumerable type, and to empty arrays for a Hash. - - $a.slice(2) |$first, $second| { ... } - - When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce - an array of two arrays with key, and value. - - $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } - $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } - - When called without a block, the function produces a concatenated result of the slices. - - slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] - slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] - slice(4,2) # produces [[0,1], [2,3]] - slice('hello',2) # produces [[h, e], [l, l], [o]] - - - Since 3.2 for Array and Hash - - Since 3.5 for additional enumerable types - - requires `parser = future`. - ENDHEREDOC - require 'puppet/parser/ast/lambda' - require 'puppet/parser/scope' - - def each_Common(o, slice_size, filler, scope, pblock) - if pblock - serving_size = pblock.last_captures_rest? ? slice_size : pblock.parameter_count - else - serving_size = 1 - end - if serving_size == 0 - raise ArgumentError, "slice(): block must define at least one parameter. Block has 0." - end - unless serving_size == 1 || serving_size == slice_size - raise ArgumentError, "slice(): block must define one parameter, or " + - "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enumerator = o.each_slice(slice_size) - result = [] - if serving_size == 1 - begin - if pblock - loop do - pblock.call(scope, enumerator.next) - end - else - loop do - result << enumerator.next - end - end - rescue StopIteration - end - else - begin - loop do - a = enumerator.next - if a.size < serving_size - a = a.dup.fill(filler, a.length...serving_size) - end - pblock.call(scope, *a) - end - rescue StopIteration - end - end - if pblock - o - else - result - end - end - - raise ArgumentError, ("slice(): wrong number of arguments (#{args.length}; must be 2 or 3)") unless args.length == 2 || args.length == 3 - if args.length >= 2 - begin - slice_size = Puppet::Parser::Scope.number?(args[1]) - rescue - raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") - end - end - raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") unless slice_size - raise ArgumentError, ("slice(): wrong argument value: #{slice_size}; is not a positive integer number > 0") unless slice_size.is_a?(Fixnum) && slice_size > 0 - receiver = args[0] - - # the block is optional, ok if nil, function then produces an array - pblock = args[2] - raise ArgumentError, ("slice(): wrong argument type (#{args[2].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) || args.length == 2 - - case receiver - when Hash - each_Common(receiver, slice_size, [], self, pblock) - else - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - if enum.nil? - raise ArgumentError, ("slice(): given type '#{tc.string(receiver)}' is not enumerable") - end - result = each_Common(enum, slice_size, :undef, self, pblock) - pblock ? receiver : result - end -end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index 740fe95af..076e80569 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,546 +1,546 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) optionally_fail(issue, semantic, options, except) # an error should have been raised since fail always fails raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end # Optionally (based on severity) Fails the evaluation of _semantic_ with a given issue # If the given issue is configured to be of severity < :error it is only reported, and the function returns. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def optionally_fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { return scope.lookupvar(name.to_s) } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, o, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end # Horrible cheat while waiting for iterative functions to be 4x FUNCTIONS_4x = { 'map' => true, 'each'=>true, 'filter' => true, 'reduce' => true, 'slice' => true } def call_function(name, args, o, scope) # Call via 4x API if it is available, and the function exists # - if loaders = Puppet.lookup(:loaders) {nil} + if loaders = scope.compiler.loaders # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader if loader && func = loader.load(:function, name) return func.call(scope, *args) end end fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) # TODO: if Puppet[:biff] == true, then 3x functions should be called via loaders above # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # Do not map the iterative functions, they are capable of dealing with 4x API, and they do # call out to lambdas, and thus, given arguments needs to be preserved (instead of transforming to # '' when undefined). TODO: The iterative functions should be refactored to use the new function API # directly, when this has been done, this special filtering out can be removed # NOTE: Passing an empty string last converts :undef to empty string mapped_args = FUNCTIONS_4x[name] ? args : args.map {|a| convert(a, scope, '') } result = scope.send("function_#{name}", mapped_args) # Prevent non r-value functions from leaking their result (they are not written to care about this) Puppet::Parser::Functions.rvalue?(name) ? result : nil end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class' # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(capitalize_qualified_name(type_name), evaluated_parameters) end # Capitalizes each segment of a qualified name # def capitalize_qualified_name(name) name.split(/::/).map(&:capitalize).join('::') end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taken place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) # This gets the parameter value, or nil (for both valid parameters and parameters that do not exist). val = resource[parameter_name] if val.nil? && defaults = scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. # NOTE: meta parameters that are unset ends up here, and there are no defaults for those encoded # in the defaults, they may receive hardcoded defaults later (e.g. 'tag'). param = defaults[parameter_name.to_sym] # Some parameters (meta parameters like 'tag') does not return a param from which the value can be obtained # at all times. Instead, they return a nil param until a value has been set. val = param.nil? ? nil : param.value end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? type_calculator.infer(resource) end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert_Object(o, scope, undef_value) o end def convert_Array(o, scope, undef_value) o.map {|x| convert(x, scope, undef_value) } end def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) } result end def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end def convert_Symbol(o, scope, undef_value) case o when :undef undef_value # 3x wants :undef as empty string in function else o # :default, and all others are verbatim since they are new in future evaluator end end def convert_PAbstractType(o, scope, undef_value) o end def convert_PCatalogEntryType(o, scope, undef_value) # Since 4x does not support dynamic scoping, all names are absolute and can be # used as is (with some check/transformation/mangling between absolute/relative form # due to Puppet::Resource's idiosyncratic behavior where some references must be # absolute and others cannot be. # Thus there is no need to call scope.resolve_type_and_titles to do dynamic lookup. Puppet::Resource.new(*catalog_type_to_split_type_title(o)) end private # Produces an array with [type, title] from a PCatalogEntryType # This method is used to produce the arguments for creation of reference resource instances # (used when 3x is operating on a resource). # Ensures that resources are *not* absolute. # def catalog_type_to_split_type_title(catalog_type) split_type = catalog_type.is_a?(Puppet::Pops::Types::PType) ? catalog_type.type : catalog_type case split_type when Puppet::Pops::Types::PHostClassType class_name = split_type.class_name ['class', class_name.nil? ? nil : class_name.sub(/^::/, '')] when Puppet::Pops::Types::PResourceType type_name = split_type.type_name title = split_type.title if type_name =~ /^(::)?[Cc]lass/ ['class', title.nil? ? nil : title.sub(/^::/, '')] else # Ensure that title is '' if nil # Resources with absolute name always results in error because tagging does not support leading :: [type_name.nil? ? nil : type_name.sub(/^::/, ''), title.nil? ? '' : title] end else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it represents neither a PHostClassType, nor a PResourceType." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self # Issues triggering warning only if --debug is on if Puppet[:debug] p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning else p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore end end end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:", :emit_warnings => true }) if errors? raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end end class EvaluationError < StandardError end end diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index 46de35b7b..7ae9d1f8c 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,1601 +1,1601 @@ # The TypeCalculator can answer questions about puppet types. # # The Puppet type system is primarily based on sub-classing. When asking the type calculator to infer types from Ruby in general, it # may not provide the wanted answer; it does not for instance take module inclusions and extensions into account. In general the type # system should be unsurprising for anyone being exposed to the notion of type. The type `Data` may require a bit more explanation; this # is an abstract type that includes all scalar types, as well as Array with an element type compatible with Data, and Hash with key # compatible with scalar and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with # the exception that the Puppet type system also includes Pattern (regular expression) as a scalar. # # Inference # --------- # The `infer(o)` method infers a Puppet type for scalar Ruby objects, and for Arrays and Hashes. # The inference result is instance specific for single typed collections # and allows answering questions about its embedded type. It does not however preserve multiple types in # a collection, and can thus not answer questions like `[1,a].infer() =~ Array[Integer, String]` since the inference # computes the common type Scalar when combining Integer and String. # # The `infer_generic(o)` method infers a generic Puppet type for scalar Ruby object, Arrays and Hashes. # This inference result does not contain instance specific information; e.g. Array[Integer] where the integer # range is the generic default. Just `infer` it also combines types into a common type. # # The `infer_set(o)` method works like `infer` but preserves all type information. It does not do any # reduction into common types or ranges. This method of inference is best suited for answering questions # about an object being an instance of a type. It correctly answers: `[1,a].infer_set() =~ Array[Integer, String]` # # The `generalize!(t)` method modifies an instance specific inference result to a generic. The method mutates # the given argument. Basically, this removes string instances from String, and range from Integer and Float. # # Assignability # ------------- # The `assignable?(t1, t2)` method answers if t2 conforms to t1. The type t2 may be an instance, in which case # its type is inferred, or a type. # # Instance? # --------- # The `instance?(t, o)` method answers if the given object (instance) is an instance that is assignable to the given type. # # String # ------ # Creates a string representation of a type. # # Creation of Type instances # -------------------------- # Instance of the classes in the {Puppet::Pops::Types type model} are used to denote a specific type. It is most convenient # to use the {Puppet::Pops::Types::TypeFactory TypeFactory} when creating instances. # # @note # In general, new instances of the wanted type should be created as they are assigned to models using containment, and a # contained object can only be in one container at a time. Also, the type system may include more details in each type # instance, such as if it may be nil, be empty, contain a certain count etc. Or put differently, the puppet types are not # singletons. # # All types support `copy` which should be used when assigning a type where it is unknown if it is bound or not # to a parent type. A check can be made with `t.eContainer().nil?` # # Equality and Hash # ----------------- # Type instances are equal in terms of Ruby eql? and `==` if they describe the same type, but they are not `equal?` if they are not # the same type instance. Two types that describe the same type have identical hash - this makes them usable as hash keys. # # Types and Subclasses # -------------------- # In general, the type calculator should be used to answer questions if a type is a subtype of another (using {#assignable?}, or # {#instance?} if the question is if a given object is an instance of a given type (or is a subtype thereof). # Many of the types also have a Ruby subtype relationship; e.g. PHashType and PArrayType are both subtypes of PCollectionType, and # PIntegerType, PFloatType, PStringType,... are subtypes of PScalarType. Even if it is possible to answer certain questions about # type by looking at the Ruby class of the types this is considered an implementation detail, and such checks should in general # be performed by the type_calculator which implements the type system semantics. # # The PRubyType # ------------- # The PRubyType corresponds to a Ruby Class, except for the puppet types that are specialized (i.e. PRubyType should not be # used for Integer, String, etc. since there are specialized types for those). # When the type calculator deals with PRubyTypes and checks for assignability, it determines the "common ancestor class" of two classes. # This check is made based on the superclasses of the two classes being compared. In order to perform this, the classes must be present # (i.e. they are resolved from the string form in the PRubyType to a loaded, instantiated Ruby Class). In general this is not a problem, # since the question to produce the common super type for two objects means that the classes must be present or there would have been # no instances present in the first place. If however the classes are not present, the type calculator will fall back and state that # the two types at least have Object in common. # # @see Puppet::Pops::Types::TypeFactory TypeFactory for how to create instances of types # @see Puppet::Pops::Types::TypeParser TypeParser how to construct a type instance from a String # @see Puppet::Pops::Types Types for details about the type model # # Using the Type Calculator # ----- # The type calculator can be directly used via its class methods. If doing time critical work and doing many # calls to the type calculator, it is more performant to create an instance and invoke the corresponding # instance methods. Note that inference is an expensive operation, rather than infering the same thing # several times, it is in general better to infer once and then copy the result if mutation to a more generic form is # required. # # @api public # class Puppet::Pops::Types::TypeCalculator Types = Puppet::Pops::Types TheInfinity = 1.0 / 0.0 # because the Infinity symbol is not defined # @api public def self.assignable?(t1, t2) singleton.assignable?(t1,t2) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # @param callable [Puppet::Pops::Types::PCallableType] - the callable # @param args [Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] args optionally including a lambda callable at the end # @return [Boolan] true if the callable accepts the arguments # # @api public def self.callable?(callable, args) singleton.callable?(callable, args) end # Produces a String representation of the given type. # @param t [Puppet::Pops::Types::PAbstractType] the type to produce a string form # @return [String] the type in string form # # @api public # def self.string(t) singleton.string(t) end # @api public def self.infer(o) singleton.infer(o) end # @api public def self.generalize!(o) singleton.generalize!(o) end # @api public def self.infer_set(o) singleton.infer_set(o) end # @api public def self.debug_string(t) singleton.debug_string(t) end # @api public def self.enumerable(t) singleton.enumerable(t) end # @api private def self.singleton() @tc_instance ||= new end # @api public # def initialize @@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1) @@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0) @@infer_set_visitor ||= Puppet::Pops::Visitor.new(nil,"infer_set",0,0) @@instance_of_visitor ||= Puppet::Pops::Visitor.new(nil,"instance_of",1,1) @@string_visitor ||= Puppet::Pops::Visitor.new(nil,"string",0,0) @@inspect_visitor ||= Puppet::Pops::Visitor.new(nil,"debug_string",0,0) @@enumerable_visitor ||= Puppet::Pops::Visitor.new(nil,"enumerable",0,0) @@extract_visitor ||= Puppet::Pops::Visitor.new(nil,"extract",0,0) @@generalize_visitor ||= Puppet::Pops::Visitor.new(nil,"generalize",0,0) @@callable_visitor ||= Puppet::Pops::Visitor.new(nil,"callable",1,1) da = Types::PArrayType.new() da.element_type = Types::PDataType.new() @data_array = da h = Types::PHashType.new() h.element_type = Types::PDataType.new() h.key_type = Types::PScalarType.new() @data_hash = h @data_t = Types::PDataType.new() @scalar_t = Types::PScalarType.new() @numeric_t = Types::PNumericType.new() @t = Types::PObjectType.new() # Data accepts a Tuple that has 0-infinity Data compatible entries (e.g. a Tuple equivalent to Array). data_tuple = Types::PTupleType.new() data_tuple.addTypes(Types::PDataType.new()) data_tuple.size_type = Types::PIntegerType.new() data_tuple.size_type.from = 0 data_tuple.size_type.to = nil # infinity @data_tuple_t = data_tuple # Variant type compatible with Data data_variant = Types::PVariantType.new() data_variant.addTypes(@data_hash.copy) data_variant.addTypes(@data_array.copy) data_variant.addTypes(Types::PScalarType.new) data_variant.addTypes(Types::PNilType.new) data_variant.addTypes(@data_tuple_t.copy) @data_variant_t = data_variant collection_default_size = Types::PIntegerType.new() collection_default_size.from = 0 collection_default_size.to = nil # infinity @collection_default_size_t = collection_default_size non_empty_string = Types::PStringType.new non_empty_string.size_type = Types::PIntegerType.new() non_empty_string.size_type.from = 1 non_empty_string.size_type.to = nil # infinity @non_empty_string_t = non_empty_string @nil_t = Types::PNilType.new end # Convenience method to get a data type for comparisons # @api private the returned value may not be contained in another element # def data @data_t end # Convenience method to get a variant compatible with the Data type. # @api private the returned value may not be contained in another element # def data_variant @data_variant_t end def self.data_variant singleton.data_variant end # Answers the question 'is it possible to inject an instance of the given class' # A class is injectable if it has a special *assisted inject* class method called `inject` taking # an injector and a scope as argument, or if it has a zero args `initialize` method. # # @param klazz [Class, PRubyType] the class/type to check if it is injectable # @return [Class, nil] the injectable Class, or nil if not injectable # @api public # def injectable_class(klazz) # Handle case when we get a PType instead of a class if klazz.is_a?(Types::PRubyType) klazz = Puppet::Pops::Types::ClassLoader.provide(klazz) end # data types can not be injected (check again, it is not safe to assume that given RubyType klazz arg was ok) return false unless type(klazz).is_a?(Types::PRubyType) if (klazz.respond_to?(:inject) && klazz.method(:inject).arity() == -4) || klazz.instance_method(:initialize).arity() == 0 klazz else nil end end # Answers 'can an instance of type t2 be assigned to a variable of type t'. # Does not accept nil/undef unless the type accepts it. # # @api public # def assignable?(t, t2) if t.is_a?(Class) t = type(t) end if t2.is_a?(Class) t2 = type(t2) end @@assignable_visitor.visit_this_1(self, t, t2) end # Returns an enumerable if the t represents something that can be iterated def enumerable(t) @@enumerable_visitor.visit_this_0(self, t) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # def callable?(callable, args) return false if !callable.is_a?(Types::PCallableType) # Note that polymorphism is for the args type, the callable is always a callable @@callable_visitor.visit_this_1(self, args, callable) end # Answers if the two given types describe the same type def equals(left, right) return false unless left.is_a?(Types::PAbstractType) && right.is_a?(Types::PAbstractType) # Types compare per class only - an extra test must be made if the are mutually assignable # to find all types that represent the same type of instance # left == right || (assignable?(right, left) && assignable?(left, right)) end # Answers 'what is the Puppet Type corresponding to the given Ruby class' # @param c [Class] the class for which a puppet type is wanted # @api public # def type(c) raise ArgumentError, "Argument must be a Class" unless c.is_a? Class # Can't use a visitor here since we don't have an instance of the class case when c <= Integer type = Types::PIntegerType.new() when c == Float type = Types::PFloatType.new() when c == Numeric type = Types::PNumericType.new() when c == String type = Types::PStringType.new() when c == Regexp type = Types::PRegexpType.new() when c == NilClass type = Types::PNilType.new() when c == FalseClass, c == TrueClass type = Types::PBooleanType.new() when c == Class type = Types::PType.new() when c == Array # Assume array of data values type = Types::PArrayType.new() type.element_type = Types::PDataType.new() when c == Hash # Assume hash with scalar keys and data values type = Types::PHashType.new() type.key_type = Types::PScalarType.new() type.element_type = Types::PDataType.new() else type = Types::PRubyType.new() type.ruby_class = c.name end type end # Generalizes value specific types. The given type is mutated and returned. # @api public def generalize!(o) @@generalize_visitor.visit_this_0(self, o) o.eAllContents.each { |x| @@generalize_visitor.visit_this_0(self, x) } o end def generalize_Object(o) # do nothing, there is nothing to change for most types end def generalize_PStringType(o) o.values = [] o.size_type = nil [] end def generalize_PCollectionType(o) # erase the size constraint from Array and Hash (if one exists, it is transformed to -Infinity - + Infinity, which is # not desirable. o.size_type = nil end def generalize_PFloatType(o) o.to = nil o.from = nil end def generalize_PIntegerType(o) o.to = nil o.from = nil end # Answers 'what is the single common Puppet Type describing o', or if o is an Array or Hash, what is the # single common type of the elements (or keys and elements for a Hash). # @api public # def infer(o) @@infer_visitor.visit_this_0(self, o) end def infer_generic(o) result = generalize!(infer(o)) result end # Answers 'what is the set of Puppet Types of o' # @api public # def infer_set(o) @@infer_set_visitor.visit_this_0(self, o) end def instance_of(t, o) @@instance_of_visitor.visit_this_1(self, t, o) end def instance_of_Object(t, o) # Undef is Undef and Object, but nothing else when checking instance? return false if (o.nil? || o == :undef) && t.class != Types::PObjectType assignable?(t, infer(o)) end def instance_of_PArrayType(t, o) return false unless o.is_a?(Array) return false unless o.all? {|element| instance_of(t.element_type, element) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) assignable?(size_t, size_t2) end def instance_of_PTupleType(t, o) return false unless o.is_a?(Array) # compute the tuple's min/max size, and check if that size matches size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) # compute the array's size as type size_t2 = size_as_type(o) return false unless assignable?(size_t, size_t2) o.each_with_index do |element, index| return false unless instance_of(t.types[index] || t.types[-1], element) end true end def instance_of_PStructType(t, o) return false unless o.is_a?(Hash) h = t.hashed_elements # all keys must be present and have a value (even if nil/undef) (o.keys - h.keys).empty? && h.all? { |k,v| instance_of(v, o[k]) } end def instance_of_PHashType(t, o) return false unless o.is_a?(Hash) key_t = t.key_type element_t = t.element_type return false unless o.keys.all? {|key| instance_of(key_t, key) } && o.values.all? {|value| instance_of(element_t, value) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) assignable?(size_t, size_t2) end def instance_of_PDataType(t, o) instance_of(@data_variant_t, o) end def instance_of_PNilType(t, o) return o.nil? || o == :undef end def instance_of_POptionalType(t, o) return true if (o.nil? || o == :undef) instance_of(t.optional_type, o) end def instance_of_PVariantType(t, o) # instance of variant if o is instance? of any of variant's types t.types.any? { |option_t| instance_of(option_t, o) } end # Answers 'is o an instance of type t' # @api public # def self.instance?(t, o) singleton.instance_of(t,o) end # Answers 'is o an instance of type t' # @api public # def instance?(t, o) instance_of(t,o) end # Answers if t is a puppet type # @api public # def is_ptype?(t) return t.is_a?(Types::PAbstractType) end # Answers if t represents the puppet type PNilType # @api public # def is_pnil?(t) return t.nil? || t.is_a?(Types::PNilType) end # Answers, 'What is the common type of t1 and t2?' # # TODO: The current implementation should be optimized for performance # # @api public # def common_type(t1, t2) raise ArgumentError, 'two types expected' unless (is_ptype?(t1) || is_pnil?(t1)) && (is_ptype?(t2) || is_pnil?(t2)) # if either is nil, the common type is the other if is_pnil?(t1) return t2 elsif is_pnil?(t2) return t1 end # Simple case, one is assignable to the other if assignable?(t1, t2) return t1 elsif assignable?(t2, t1) return t2 end # when both are arrays, return an array with common element type if t1.is_a?(Types::PArrayType) && t2.is_a?(Types::PArrayType) type = Types::PArrayType.new() type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are hashes, return a hash with common key- and element type if t1.is_a?(Types::PHashType) && t2.is_a?(Types::PHashType) type = Types::PHashType.new() type.key_type = common_type(t1.key_type, t2.key_type) type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are host-classes, reduce to PHostClass[] (since one was not assignable to the other) if t1.is_a?(Types::PHostClassType) && t2.is_a?(Types::PHostClassType) return Types::PHostClassType.new() end # when both are resources, reduce to Resource[T] or Resource[] (since one was not assignable to the other) if t1.is_a?(Types::PResourceType) && t2.is_a?(Types::PResourceType) result = Types::PResourceType.new() # only Resource[] unless the type name is the same if t1.type_name == t2.type_name then result.type_name = t1.type_name end # the cross assignability test above has already determined that they do not have the same type and title return result end # Integers have range, expand the range to the common range if t1.is_a?(Types::PIntegerType) && t2.is_a?(Types::PIntegerType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PIntegerType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end # Floats have range, expand the range to the common range if t1.is_a?(Types::PFloatType) && t2.is_a?(Types::PFloatType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PFloatType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end if t1.is_a?(Types::PStringType) && t2.is_a?(Types::PStringType) t = Types::PStringType.new() t.values = t1.values | t2.values return t end if t1.is_a?(Types::PPatternType) && t2.is_a?(Types::PPatternType) t = Types::PPatternType.new() # must make copies since patterns are contained types, not data-types t.patterns = (t1.patterns | t2.patterns).map {|p| p.copy } return t end if t1.is_a?(Types::PEnumType) && t2.is_a?(Types::PEnumType) # The common type is one that complies with either set t = Types::PEnumType.new t.values = t1.values | t2.values return t end if t1.is_a?(Types::PVariantType) && t2.is_a?(Types::PVariantType) # The common type is one that complies with either set t = Types::PVariantType.new t.types = (t1.types | t2.types).map {|opt_t| opt_t.copy } return t end if t1.is_a?(Types::PRegexpType) && t2.is_a?(Types::PRegexpType) # if they were identical, the general rule would return a parameterized regexp # since they were not, the result is a generic regexp type return Types::PPatternType.new() end if t1.is_a?(Types::PCallableType) && t2.is_a?(Types::PCallableType) # They do not have the same signature, and one is not assignable to the other, # what remains is the most general form of Callable return Types::PCallableType.new() end # Common abstract types, from most specific to most general if common_numeric?(t1, t2) return Types::PNumericType.new() end if common_scalar?(t1, t2) return Types::PScalarType.new() end if common_data?(t1,t2) return Types::PDataType.new() end # Meta types Type[Integer] + Type[String] => Type[Data] if t1.is_a?(Types::PType) && t2.is_a?(Types::PType) type = Types::PType.new() type.type = common_type(t1.type, t2.type) return type end if t1.is_a?(Types::PRubyType) && t2.is_a?(Types::PRubyType) if t1.ruby_class == t2.ruby_class return t1 end # finding the common super class requires that names are resolved to class c1 = Types::ClassLoader.provide_from_type(t1) c2 = Types::ClassLoader.provide_from_type(t2) if c1 && c2 c2_superclasses = superclasses(c2) superclasses(c1).each do|c1_super| c2_superclasses.each do |c2_super| if c1_super == c2_super result = Types::PRubyType.new() result.ruby_class = c1_super.name return result end end end end end # If both are RubyObjects if common_pobject?(t1, t2) return Types::PObjectType.new() end end # Produces the superclasses of the given class, including the class def superclasses(c) result = [c] while s = c.superclass result << s c = s end result end # Produces a string representing the type # @api public # def string(t) @@string_visitor.visit_this_0(self, t) end # Produces a debug string representing the type (possibly with more information that the regular string format) # @api public # def debug_string(t) @@inspect_visitor.visit_this_0(self, t) end # Reduces an enumerable of types to a single common type. # @api public # def reduce_type(enumerable) enumerable.reduce(nil) {|memo, t| common_type(memo, t) } end # Reduce an enumerable of objects to a single common type # @api public # def infer_and_reduce_type(enumerable) reduce_type(enumerable.collect() {|o| infer(o) }) end # The type of all classes is PType # @api private # def infer_Class(o) Types::PType.new() end # @api private def infer_Closure(o) o.type() end # @api private def infer_Function(o) o.class.dispatcher.to_type end # @api private def infer_Object(o) type = Types::PRubyType.new() type.ruby_class = o.class.name type end # The type of all types is PType # @api private # def infer_PAbstractType(o) type = Types::PType.new() type.type = o.copy type end # The type of all types is PType # This is the metatype short circuit. # @api private # def infer_PType(o) type = Types::PType.new() type.type = o.copy type end # @api private def infer_String(o) t = Types::PStringType.new() t.addValues(o) t.size_type = size_as_type(o) t end # @api private def infer_Float(o) t = Types::PFloatType.new() t.from = o t.to = o t end # @api private def infer_Integer(o) t = Types::PIntegerType.new() t.from = o t.to = o t end # @api private def infer_Regexp(o) t = Types::PRegexpType.new() t.pattern = o.source t end # @api private def infer_NilClass(o) Types::PNilType.new() end # Inference of :undef as PNilType, all other are Ruby[Symbol] # @api private def infer_Symbol(o) o == :undef ? infer_NilClass(o) : infer_Object(o) end # @api private def infer_TrueClass(o) Types::PBooleanType.new() end # @api private def infer_FalseClass(o) Types::PBooleanType.new() end # @api private # A Puppet::Parser::Resource, or Puppet::Resource # def infer_Resource(o) t = Types::PResourceType.new() t.type_name = o.type.to_s.downcase # Only Puppet::Resource can have a title that is a symbol :undef, a PResource cannot. # A mapping must be made to empty string. A nil value will result in an error later title = o.title t.title = (title == :undef ? '' : title) type = Types::PType.new() type.type = t type end # @api private def infer_Array(o) type = Types::PArrayType.new() type.element_type = if o.empty? Types::PNilType.new() else infer_and_reduce_type(o) end type.size_type = size_as_type(o) type end # @api private def infer_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() etype = Types::PNilType.new() else ktype = infer_and_reduce_type(o.keys()) etype = infer_and_reduce_type(o.values()) end type.key_type = ktype type.element_type = etype type.size_type = size_as_type(o) type end def size_as_type(collection) size = collection.size t = Types::PIntegerType.new() t.from = size t.to = size t end # Common case for everything that intrinsically only has a single type def infer_set_Object(o) infer(o) end def infer_set_Array(o) if o.empty? type = Types::PArrayType.new() type.element_type = Types::PNilType.new() type.size_type = size_as_type(o) else type = Types::PTupleType.new() type.types = o.map() {|x| infer_set(x) } end type end def infer_set_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() - etype = Types::PNilType.new() + vtype = Types::PNilType.new() else ktype = Types::PVariantType.new() ktype.types = o.keys.map() {|k| infer_set(k) } etype = Types::PVariantType.new() etype.types = o.values.map() {|e| infer_set(e) } end type.key_type = unwrap_single_variant(ktype) - type.element_type = unwrap_single_variant(vtype) + type.element_type = unwrap_single_variant(etype) type.size_type = size_as_type(o) type end def unwrap_single_variant(possible_variant) if possible_variant.is_a?(Types::PVariantType) && possible_variant.types.size == 1 possible_variant.types[0] else possible_variant end end # False in general type calculator # @api private def assignable_Object(t, t2) false end # @api private def assignable_PObjectType(t, t2) t2.is_a?(Types::PObjectType) end # @api private def assignable_PNilType(t, t2) # Only undef/nil is assignable to nil type t2.is_a?(Types::PNilType) end # @api private def assignable_PScalarType(t, t2) t2.is_a?(Types::PScalarType) end # @api private def assignable_PNumericType(t, t2) t2.is_a?(Types::PNumericType) end # @api private def assignable_PIntegerType(t, t2) return false unless t2.is_a?(Types::PIntegerType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # Transform int range to a size constraint # if range == nil the constraint is 1,1 # if range.from == nil min size = 1 # if range.to == nil max size == Infinity # def size_range(range) return [1,1] if range.nil? from = range.from to = range.to x = from.nil? ? 1 : from y = to.nil? ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def from_to_ordered(from, to) x = (from.nil? || from == :default) ? -TheInfinity : from y = (to.nil? || to == :default) ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def assignable_PVariantType(t, t2) # Data is a specific variant t2 = @data_variant_t if t2.is_a?(Types::PDataType) if t2.is_a?(Types::PVariantType) # A variant is assignable if all of its options are assignable to one of this type's options return true if t == t2 t2.types.all? do |other| # if the other is a Variant, all if its options, but be assignable to one of this type's options other = other.is_a?(Types::PDataType) ? @data_variant_t : other if other.is_a?(Types::PVariantType) assignable?(t, other) else t.types.any? {|option_t| assignable?(option_t, other) } end end else # A variant is assignable if t2 is assignable to any of its types t.types.any? { |option_t| assignable?(option_t, t2) } end end # Catch all not callable combinations def callable_Object(o, callable_t) false end def callable_PTupleType(args_tuple, callable_t) if args_tuple.size_type raise ArgumentError, "Callable tuple may not have a size constraint when used as args" end # Assume no block was given - i.e. it is nil, and its type is PNilType block_t = @nil_t if args_tuple.types.last.is_a?(Types::PCallableType) # a split is needed to make it possible to use required, optional, and varargs semantics # of the tuple type. # args_tuple = args_tuple.copy # to drop the callable, it must be removed explicitly since this is an rgen array args_tuple.removeTypes(block_t = args_tuple.types.last()) else # no block was given, if it is required, the below will fail end # unless argument types match parameter types return false unless assignable?(callable_t.param_types, args_tuple) # unless given block (or no block) matches expected block (or no block) assignable?(callable_t.block_type || @nil_t, block_t) end def callable_PArrayType(args_array, callable_t) return false unless assignable?(callable_t.param_types, args_array) # does not support calling with a block, but have to check that callable expects it assignable?(callable_t.block_type || @nil_t, @nil_t) end def max(a,b) a >=b ? a : b end def min(a,b) a <= b ? a : b end def assignable_PTupleType(t, t2) return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType)) size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) if t2.is_a?(Types::PTupleType) size_t2 = t2.size_type || Puppet::Pops::Types::TypeFactory.range(*t2.size_range) # not assignable if the number of types in t2 is outside number of types in t1 if assignable?(size_t, size_t2) t2.types.size.times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2.types[index]) end return true else return false end elsif t2.is_a?(Types::PArrayType) t2_entry = t2.element_type # Array of anything can not be assigned (unless tuple is tuple of anything) - this case # was handled at the top of this method. # return false if t2_entry.nil? size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) size_t2 = t2.size_type || @collection_default_size_t return false unless assignable?(size_t, size_t2) min(t.types.size, size_t2.range()[1]).times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2_entry) end true else false end end # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last # type, and an index. # Produces nil if the index is out of bounds # from must be less than to, and from may not be less than 0 # # @api private # def tuple_entry_at(tuple_t, from, to, index) regular = (tuple_t.types.size - 1) if index < regular tuple_t.types[index] elsif index < regular + to # in the varargs part tuple_t.types[-1] else nil end end # @api private # def assignable_PStructType(t, t2) return true if t == t2 || t.elements.empty? && (t2.is_a?(Types::PHashType)) h = t.hashed_elements if t2.is_a?(Types::PStructType) h2 = t2.hashed_elements h.size == h2.size && h.all? {|k, v| assignable?(v, h2[k]) } elsif t2.is_a?(Types::PHashType) size_t2 = t2.size_type || @collection_default_size_t size_t = Types::PIntegerType.new size_t.from = size_t.to = h.size # compatible size # hash key type must be string of min 1 size # hash value t must be assignable to each key element_type = t2.element_type assignable?(size_t, size_t2) && assignable?(@non_empty_string_t, t2.key_type) && h.all? {|k,v| assignable?(v, element_type) } else false end end # @api private def assignable_POptionalType(t, t2) return true if t2.is_a?(Types::PNilType) if t2.is_a?(Types::POptionalType) assignable?(t.optional_type, t2.optional_type) else assignable?(t.optional_type, t2) end end # @api private def assignable_PEnumType(t, t2) return true if t == t2 || (t.values.empty? && (t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType))) if t2.is_a?(Types::PStringType) # if the set of strings are all found in the set of enums t2.values.all? { |s| t.values.any? { |e| e == s }} else false end end # @api private def assignable_PStringType(t, t2) if t.values.empty? # A general string is assignable by any other string or pattern restricted string # if the string has a size constraint it does not match since there is no reasonable way # to compute the min/max length a pattern will match. For enum, it is possible to test that # each enumerator value is within range size_t = t.size_type || @collection_default_size_t case t2 when Types::PStringType # true if size compliant size_t2 = t2.size_type || @collection_default_size_t assignable?(size_t, size_t2) when Types::PPatternType # true if size constraint is at least 0 to +Infinity (which is the same as the default) assignable?(size_t, @collection_default_size_t) when Types::PEnumType if t2.values # true if all enum values are within range min, max = t2.values.map(&:size).minmax trange = from_to_ordered(size_t.from, size_t.to) t2range = [min, max] # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] else # no string can match this enum anyway since it does not accept anything false end else # no other type matches string false end elsif t2.is_a?(Types::PStringType) # A specific string acts as a set of strings - must have exactly the same strings # In this case, size does not matter since the definition is very precise anyway Set.new(t.values) == Set.new(t2.values) else # All others are false, since no other type describes the same set of specific strings false end end # @api private def assignable_PPatternType(t, t2) return true if t == t2 return false unless t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType) if t2.values.empty? # Strings / Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok # (There should really always be a pattern, but better safe than sorry). return t.patterns.empty? ? true : false end # all strings in String/Enum type must match one of the patterns in Pattern type regexps = t.patterns.map {|p| p.regexp } t2.values.all? { |v| regexps.any? {|re| re.match(v) } } end # @api private def assignable_PFloatType(t, t2) return false unless t2.is_a?(Types::PFloatType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # @api private def assignable_PBooleanType(t, t2) t2.is_a?(Types::PBooleanType) end # @api private def assignable_PRegexpType(t, t2) t2.is_a?(Types::PRegexpType) && (t.pattern.nil? || t.pattern == t2.pattern) end # @api private def assignable_PCallableType(t, t2) return false unless t2.is_a?(Types::PCallableType) # nil param_types means, any other Callable is assignable return true if t.param_types.nil? return false unless assignable?(t.param_types, t2.param_types) # names are ignored, they are just information # Blocks must be compatible this_block_t = t.block_type || @nil_t that_block_t = t2.block_type || @nil_t assignable?(this_block_t, that_block_t) end # @api private def assignable_PCollectionType(t, t2) size_t = t.size_type || @collection_default_size_t case t2 when Types::PCollectionType size_t2 = t2.size_type || @collection_default_size_t assignable?(size_t, size_t2) when Types::PTupleType # compute the tuple's min/max size, and check if that size matches from, to = size_range(t2.size_type) t2s = Types::PIntegerType.new() t2s.from = t2.types.size - 1 + from t2s.to = t2.types.size - 1 + to assignable?(size_t, t2s) when Types::PStructType from = to = t2.elements.size t2s = Types::PIntegerType.new() t2s.from = from t2s.to = to assignable?(size_t, t2s) else false end end # @api private def assignable_PType(t, t2) return false unless t2.is_a?(Types::PType) return true if t.type.nil? # wide enough to handle all types return false if t2.type.nil? # wider than t assignable?(t.type, t2.type) end # Array is assignable if t2 is an Array and t2's element type is assignable, or if t2 is a Tuple # where # @api private def assignable_PArrayType(t, t2) if t2.is_a?(Types::PArrayType) return false unless assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) elsif t2.is_a?(Types::PTupleType) return false unless t2.types.all? {|t2_element| assignable?(t.element_type, t2_element) } t2_regular = t2.types[0..-2] t2_ranged = t2.types[-1] t2_from, t2_to = size_range(t2.size_type) t2_required = t2_regular.size + t2_from t_entry = t.element_type # Tuple of anything can not be assigned (unless array is tuple of anything) - this case # was handled at the top of this method. # return false if t_entry.nil? # array type may be size constrained size_t = t.size_type || @collection_default_size_t min, max = size_t.range # Tuple with fewer min entries can not be assigned return false if t2_required < min # Tuple with more optionally available entries can not be assigned return false if t2_regular.size + t2_to > max # each tuple type must be assignable to the element type t2_required.times do |index| t2_entry = tuple_entry_at(t2, t2_from, t2_to, index) return false unless assignable?(t_entry, t2_entry) end # ... and so must the last, possibly optional (ranged) type return assignable?(t_entry, t2_ranged) else false end end # Hash is assignable if t2 is a Hash and t2's key and element types are assignable # @api private def assignable_PHashType(t, t2) case t2 when Types::PHashType return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) when Types::PStructType # hash must accept String as key type # hash must accept all value types # hash must accept the size of the struct size_t = t.size_type || @collection_default_size_t min, max = size_t.range struct_size = t2.elements.size element_type = t.element_type ( struct_size >= min && struct_size <= max && assignable?(t.key_type, @non_empty_string_t) && t2.hashed_elements.all? {|k,v| assignable?(element_type, v) }) else false end end # @api private def assignable_PCatalogEntryType(t1, t2) t2.is_a?(Types::PCatalogEntryType) end # @api private def assignable_PHostClassType(t1, t2) return false unless t2.is_a?(Types::PHostClassType) # Class = Class[name}, Class[name] != Class return true if t1.class_name.nil? # Class[name] = Class[name] return t1.class_name == t2.class_name end # @api private def assignable_PResourceType(t1, t2) return false unless t2.is_a?(Types::PResourceType) return true if t1.type_name.nil? return false if t1.type_name != t2.type_name return true if t1.title.nil? return t1.title == t2.title end # Data is assignable by other Data and by Array[Data] and Hash[Scalar, Data] # @api private def assignable_PDataType(t, t2) t2.is_a?(Types::PDataType) || assignable?(@data_variant_t, t2) end # Assignable if t2's ruby class is same or subclass of t1's ruby class # @api private def assignable_PRubyType(t1, t2) return false unless t2.is_a?(Types::PRubyType) return true if t1.ruby_class.nil? # t1 is wider return false if t2.ruby_class.nil? # t1 not nil, so t2 can not be wider c1 = class_from_string(t1.ruby_class) c2 = class_from_string(t2.ruby_class) return false unless c1.is_a?(Class) && c2.is_a?(Class) !!(c2 <= c1) end # @api private def debug_string_Object(t) string(t) end # @api private def string_PType(t) if t.type.nil? "Type" else "Type[#{string(t.type)}]" end end # @api private def string_NilClass(t) ; '?' ; end # @api private def string_String(t) ; t ; end # @api private def string_PObjectType(t) ; "Object" ; end # @api private def string_PNilType(t) ; 'Undef' ; end # @api private def string_PBooleanType(t) ; "Boolean" ; end # @api private def string_PScalarType(t) ; "Scalar" ; end # @api private def string_PDataType(t) ; "Data" ; end # @api private def string_PNumericType(t) ; "Numeric" ; end # @api private def string_PIntegerType(t) range = range_array_part(t) unless range.empty? "Integer[#{range.join(', ')}]" else "Integer" end end # Produces a string from an Integer range type that is used inside other type strings # @api private def range_array_part(t) return [] if t.nil? || (t.from.nil? && t.to.nil?) [t.from.nil? ? 'default' : t.from , t.to.nil? ? 'default' : t.to ] end # @api private def string_PFloatType(t) range = range_array_part(t) unless range.empty? "Float[#{range.join(', ')}]" else "Float" end end # @api private def string_PRegexpType(t) t.pattern.nil? ? "Regexp" : "Regexp[#{t.regexp.inspect}]" end # @api private def string_PStringType(t) # skip values in regular output - see debug_string range = range_array_part(t.size_type) unless range.empty? "String[#{range.join(', ')}]" else "String" end end # @api private def debug_string_PStringType(t) range = range_array_part(t.size_type) range_part = range.empty? ? '' : '[' << range.join(' ,') << '], ' "String[" << range_part << (t.values.map {|s| "'#{s}'" }).join(', ') << ']' end # @api private def string_PEnumType(t) return "Enum" if t.values.empty? "Enum[" << t.values.map {|s| "'#{s}'" }.join(', ') << ']' end # @api private def string_PVariantType(t) return "Variant" if t.types.empty? "Variant[" << t.types.map {|t2| string(t2) }.join(', ') << ']' end # @api private def string_PTupleType(t) range = range_array_part(t.size_type) return "Tuple" if t.types.empty? s = "Tuple[" << t.types.map {|t2| string(t2) }.join(', ') unless range.empty? s << ", " << range.join(', ') end s << "]" s end # @api private def string_PCallableType(t) # generic return "Callable" if t.param_types.nil? if t.param_types.types.empty? range = [0, 0] else range = range_array_part(t.param_types.size_type) end types = t.param_types.types.map {|t2| string(t2) } params_part= types.join(', ') s = "Callable[" << types.join(', ') unless range.empty? (s << ', ') unless types.empty? s << range.join(', ') end # Add block T last (after min, max) if present) # unless t.block_type.nil? (s << ', ') unless types.empty? && range.empty? s << string(t.block_type) end s << "]" s end # @api private def string_PStructType(t) return "Struct" if t.elements.empty? "Struct[{" << t.elements.map {|element| string(element) }.join(', ') << "}]" end def string_PStructElement(t) "'#{t.name}'=>#{string(t.type)}" end # @api private def string_PPatternType(t) return "Pattern" if t.patterns.empty? "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']' end # @api private def string_PCollectionType(t) range = range_array_part(t.size_type) unless range.empty? "Collection[#{range.join(', ')}]" else "Collection" end end # @api private def string_PRubyType(t) ; "Ruby[#{string(t.ruby_class)}]" ; end # @api private def string_PArrayType(t) parts = [string(t.element_type)] + range_array_part(t.size_type) "Array[#{parts.join(', ')}]" end # @api private def string_PHashType(t) parts = [string(t.key_type), string(t.element_type)] + range_array_part(t.size_type) "Hash[#{parts.join(', ')}]" end # @api private def string_PCatalogEntryType(t) "CatalogEntry" end # @api private def string_PHostClassType(t) if t.class_name "Class[#{t.class_name}]" else "Class" end end # @api private def string_PResourceType(t) if t.type_name if t.title "#{t.type_name.capitalize}['#{t.title}']" else "#{t.type_name.capitalize}" end else "Resource" end end def string_POptionalType(t) if t.optional_type.nil? "Optional" else "Optional[#{string(t.optional_type)}]" end end # Catches all non enumerable types # @api private def enumerable_Object(o) nil end # @api private def enumerable_PIntegerType(t) # Not enumerable if representing an infinite range return nil if t.size == TheInfinity t end def self.copy_as_tuple(t) case t when Types::PTupleType t.copy when Types::PArrayType # transform array to tuple result = Types::PTupleType.new result.addTypes(t.element_type.copy) result.size_type = t.size_type.nil? ? nil : t.size_type.copy result else raise ArgumentError, "Internal Error: Only Array and Tuple can be given to copy_as_tuple" end end private def class_from_string(str) begin str.split('::').inject(Object) do |memo, name_segment| memo.const_get(name_segment) end rescue NameError return nil end end def common_data?(t1, t2) assignable?(@data_t, t1) && assignable?(@data_t, t2) end def common_scalar?(t1, t2) assignable?(@scalar_t, t1) && assignable?(@scalar_t, t2) end def common_numeric?(t1, t2) assignable?(@numeric_t, t1) && assignable?(@numeric_t, t2) end def common_pobject?(t1, t2) assignable?(@t, t1) && assignable?(@t, t2) end end diff --git a/lib/puppet/util/functions/iterative_support.rb b/lib/puppet/util/functions/iterative_support.rb new file mode 100644 index 000000000..cc74154f2 --- /dev/null +++ b/lib/puppet/util/functions/iterative_support.rb @@ -0,0 +1,27 @@ +module Puppet::Util::Functions + module IterativeSupport + def asserted_serving_size(pblock, name_of_first) + size = pblock.parameter_count + if size == 0 + raise ArgumentError, "#{self.class.name}(): block must define at least one parameter; value. Block has 0." + end + if size > 2 + raise ArgumentError, "#{self.class.name}(): block must define at most two parameters; #{name_of_first}, value. Block has #{size}; "+ + pblock.parameter_names.join(', ') + end + if pblock.last_captures_rest? + # it has one or two parameters, and the last captures the rest - deliver args as if it accepts 2 + size = 2 + end + size + end + + def asserted_enumerable(obj) + unless enum = Puppet::Pops::Types::Enumeration.enumerator(obj) + raise ArgumentError, ("#{self.class.name}(): wrong argument type (#{obj.class}; must be something enumerable.") + end + enum + end + + end +end \ No newline at end of file diff --git a/spec/unit/parser/methods/each_spec.rb b/spec/unit/functions/each_spec.rb similarity index 95% rename from spec/unit/parser/methods/each_spec.rb rename to spec/unit/functions/each_spec.rb index 053d978e9..2cf7d07e7 100644 --- a/spec/unit/parser/methods/each_spec.rb +++ b/spec/unit/functions/each_spec.rb @@ -1,106 +1,110 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' -require 'rubygems' + +require 'unit/functions/shared' describe 'the each method' do include PuppetSpec::Compiler before :each do Puppet[:parser] = 'future' end context "should be callable as" do it 'each on an array selecting each value' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.each |$v| { file { "/file_$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on an array selecting each value - function call style' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] each ($a) |$index, $v| { file { "/file_$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on an array with index' do catalog = compile_to_catalog(<<-MANIFEST) $a = [present, absent, present] $a.each |$k,$v| { file { "/file_${$k+1}": ensure => $v } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'absent' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on a hash selecting entries' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>'present','b'=>'absent','c'=>'present'} $a.each |$e| { file { "/file_${e[0]}": ensure => $e[1] } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end it 'each on a hash selecting key and value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.each |$k, $v| { file { "/file_$k": ensure => $v } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end it 'each on a hash selecting key and value (using captures-last parameter)' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.each |*$kv| { file { "/file_${kv[0]}": ensure => $kv[1] } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end end context "should produce receiver" do it 'each checking produced value using single expression' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, 3, 2] $b = $a.each |$x| { "unwanted" } file { "/file_${b[1]}": ensure => present } MANIFEST catalog.resource(:file, "/file_3")['ensure'].should == 'present' end end + it_should_behave_like 'all iterative functions argument checks', 'each' + it_should_behave_like 'all iterative functions hash handling', 'each' + end diff --git a/spec/unit/parser/methods/filter_spec.rb b/spec/unit/functions/filter_spec.rb similarity index 99% rename from spec/unit/parser/methods/filter_spec.rb rename to spec/unit/functions/filter_spec.rb index 5639faff7..909837602 100644 --- a/spec/unit/parser/methods/filter_spec.rb +++ b/spec/unit/functions/filter_spec.rb @@ -1,149 +1,149 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' -require 'unit/parser/methods/shared' +require 'unit/functions/shared' describe 'the filter method' do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = 'future' end it 'should filter on an array (all berries)' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $a.filter |$x|{ $x =~ /berry$/}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'should filter on enumerable type (Integer)' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,10] $a.filter |$x|{ $x % 3 == 0}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_9]").with_parameter(:ensure, 'present') end it 'should filter on enumerable type (Integer) using two args index/value' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[10,18] $a.filter |$i, $x|{ $i % 3 == 0}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_10]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_13]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_16]").with_parameter(:ensure, 'present') end it 'should produce an array when acting on an array' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |$x|{ $x =~ /berry$/} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'can filter array using index and value' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |$index, $x|{ $index == 0 or $index ==2} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_orange]").with_parameter(:ensure, 'present') end it 'can filter array using index and value (using captures-rest)' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |*$ix|{ $ix[0] == 0 or $ix[0] ==2} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_orange]").with_parameter(:ensure, 'present') end it 'filters on a hash (all berries) by key' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} $a.filter |$x|{ $x[0] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'should produce a hash when acting on a hash' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} $b = $a.filter |$x|{ $x[0] =~ /berry$/} file { "/file_${b['strawberry']}": ensure => present } file { "/file_${b['blueberry']}": ensure => present } file { "/file_${b['orange']}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_red]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blue]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_]").with_parameter(:ensure, 'present') end it 'filters on a hash (all berries) by value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawb'=>'red berry','blueb'=>'blue berry','orange'=>'orange fruit'} $a.filter |$x|{ $x[1] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawb]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueb]").with_parameter(:ensure, 'present') end context 'filter checks arguments and' do it 'raises an error when block has more than 2 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].filter |$indexm, $x, $yikes|{ } MANIFEST end.to raise_error(Puppet::Error, /block must define at most two parameters/) end it 'raises an error when block has fewer than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].filter || { } MANIFEST end.to raise_error(Puppet::Error, /block must define at least one parameter/) end end it_should_behave_like 'all iterative functions argument checks', 'filter' it_should_behave_like 'all iterative functions hash handling', 'filter' end diff --git a/spec/unit/parser/methods/map_spec.rb b/spec/unit/functions/map_spec.rb similarity index 99% rename from spec/unit/parser/methods/map_spec.rb rename to spec/unit/functions/map_spec.rb index 34c5ac7b9..6f54648d9 100644 --- a/spec/unit/parser/methods/map_spec.rb +++ b/spec/unit/functions/map_spec.rb @@ -1,209 +1,209 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' -require 'unit/parser/methods/shared' +require 'unit/functions/shared' describe 'the map method' do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = "future" end context "using future parser" do it 'map on an array (multiplying each value by 2)' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.map |$x|{ $x*2}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_4]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on an enumerable type (multiplying each value by 2)' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,3] $a.map |$x|{ $x*2}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_4]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on an integer (multiply each by 3)' do catalog = compile_to_catalog(<<-MANIFEST) 3.map |$x|{ $x*3}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on a string' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>x, b=>y} "ab".map |$x|{$a[$x]}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_x]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_y]").with_parameter(:ensure, 'present') end it 'map on an array (multiplying value by 10 in even index position)' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.map |$i, $x|{ if $i % 2 == 0 {$x} else {$x*10}}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_20]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'map on a hash selecting keys' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$x|{ $x[0]}.each |$k|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'map on a hash selecting keys - using two block parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$k,$v|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'map on a hash using captures-last parameter' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.map |*$kv|{ file { "/file_${kv[0]}": ensure => $kv[1] } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'each on a hash selecting value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$x|{ $x[1]}.each |$k|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'each on a hash selecting value - using two block parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$k,$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end context "handles data type corner cases" do it "map gets values that are false" do catalog = compile_to_catalog(<<-MANIFEST) $a = [false,false] $a.map |$x| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.false]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_1.false]").with_parameter(:ensure, 'present') end it "map gets values that are nil" do Puppet::Parser::Functions.newfunction(:nil_array, :type => :rvalue) do |args| [nil] end catalog = compile_to_catalog(<<-MANIFEST) $a = nil_array() $a.map |$x| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.]").with_parameter(:ensure, 'present') end it "map gets values that are undef" do pending "Test is flawed, but has good intentions - should be rewritten when map has moved to new func API" # The test is broken because: # - a bug caused the given value to always be overridden by a given lambda default # - non existing variable results in nil / undef, which is transformed to empty string in the 3x func API # - when lambda is called, it gets an empty string, and it is then expected to use the default value # # This is not the semantics we want (only missing argument should trigger the default value). # Finally, it is not possible to test missing arguments with the map function since the call adapts itself # to the number of lambda parameters. (There is testing of this elsewhere). # # TODO: Rewrite map function, then test that undef / nil values are passed correctly to the lambda # catalog = compile_to_catalog(<<-MANIFEST) $a = [$does_not_exist] $a.map |$x = "something"| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_0.something")['ensure'].should == 'present' end end context 'map checks arguments and' do it 'raises an error when block has more than 2 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].map |$index, $x, $yikes|{ } MANIFEST end.to raise_error(Puppet::Error, /block must define at most two parameters/) end it 'raises an error when block has fewer than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].map || { } MANIFEST end.to raise_error(Puppet::Error, /block must define at least one parameter/) end end it_should_behave_like 'all iterative functions argument checks', 'map' it_should_behave_like 'all iterative functions hash handling', 'map' end end diff --git a/spec/unit/parser/methods/reduce_spec.rb b/spec/unit/functions/reduce_spec.rb similarity index 100% rename from spec/unit/parser/methods/reduce_spec.rb rename to spec/unit/functions/reduce_spec.rb diff --git a/spec/unit/parser/methods/shared.rb b/spec/unit/functions/shared.rb similarity index 72% rename from spec/unit/parser/methods/shared.rb rename to spec/unit/functions/shared.rb index 42cfd2359..57d2b72a7 100644 --- a/spec/unit/parser/methods/shared.rb +++ b/spec/unit/functions/shared.rb @@ -1,45 +1,45 @@ shared_examples_for 'all iterative functions hash handling' do |func| it 'passes a hash entry as an array of the key and value' do catalog = compile_to_catalog(<<-MANIFEST) {a=>1}.#{func} |$v| { notify { "${v[0]} ${v[1]}": } } MANIFEST catalog.resource(:notify, "a 1").should_not be_nil end end shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when used against an unsupported type' do expect do compile_to_catalog(<<-MANIFEST) 3.14.#{func} |$v| { } MANIFEST end.to raise_error(Puppet::Error, /must be something enumerable/) end it 'raises an error when called with any parameters besides a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}(1) |$v| { } MANIFEST - end.to raise_error(Puppet::Error, /Wrong number of arguments/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*arg count \{2\}.*actual.*arg count \{3\}/m) end it 'raises an error when called without a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}() MANIFEST - end.to raise_error(Puppet::Error, /Wrong number of arguments/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*arg count \{2\}.*actual.*arg count \{1\}/m) end - it 'raises an error when called without a block' do + it 'raises an error when called with something that is not a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}(1) MANIFEST - end.to raise_error(Puppet::Error, /must be a parameterized block/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*Callable.*actual(?!Callable\)).*/m) end end diff --git a/spec/unit/parser/methods/slice_spec.rb b/spec/unit/functions/slice_spec.rb similarity index 99% rename from spec/unit/parser/methods/slice_spec.rb rename to spec/unit/functions/slice_spec.rb index 262537045..945cae5c7 100644 --- a/spec/unit/parser/methods/slice_spec.rb +++ b/spec/unit/functions/slice_spec.rb @@ -1,149 +1,148 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' -require 'rubygems' require 'matchers/resource' describe 'methods' do include PuppetSpec::Compiler include Matchers::Resource before :all do # enable switching back @saved_parser = Puppet[:parser] # These tests only work with future parser Puppet[:parser] = 'future' end after :all do # switch back to original Puppet[:parser] = @saved_parser end before :each do node = Puppet::Node.new("floppy", :environment => 'production') @compiler = Puppet::Parser::Compiler.new(node) @scope = Puppet::Parser::Scope.new(@compiler) @topscope = @scope.compiler.topscope @scope.parent = @topscope Puppet[:parser] = 'future' end context "should be callable on array as" do it 'slice with explicit parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |$k,$v| { file { "/file_${$k}": ensure => $v } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with captures last' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |*$kv| { file { "/file_${$kv[0]}": ensure => $kv[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with one parameter' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with shorter last slice' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, present, 3, absent] $a.slice(4) |$a, $b, $c, $d| { file { "/file_$a.$c": ensure => $b } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.]").with_parameter(:ensure, 'absent') end end context "should be callable on hash as" do it 'slice with explicit parameters, missing are empty' do catalog = compile_to_catalog(<<-MANIFEST) $a = {1=>present, 2=>present, 3=>absent} $a.slice(2) |$a,$b| { file { "/file_${a[0]}.${b[0]}": ensure => $a[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.]").with_parameter(:ensure, 'absent') end end context "should be callable on enumerable types as" do it 'slice with integer range' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,4] $a.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.4]").with_parameter(:ensure, 'present') end it 'slice with integer' do catalog = compile_to_catalog(<<-MANIFEST) 4.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2.3]").with_parameter(:ensure, 'present') end it 'slice with string' do catalog = compile_to_catalog(<<-MANIFEST) 'abcd'.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a.b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c.d]").with_parameter(:ensure, 'present') end end context "when called without a block" do it "should produce an array with the result" do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2).each |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end end end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index 8477aa0f2..65fb8395e 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1214 +1,1218 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'puppet/parser/e4_parser_adapter' # relative to this spec file (./) does not work as this file is loaded by rspec #require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do include PuppetSpec::Pops include PuppetSpec::Scope before(:each) do Puppet[:strict_variables] = true - # These must be set since the is 3x logic that triggers on these even if the tests are explicit - # about selection of parser and evaluator + # These must be set since the 3x logic switches some behaviors on these even if the tests explicitly + # use the 4x parser and evaluator. # Puppet[:parser] = 'future' Puppet[:evaluator] = 'future' + # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) require 'puppetx' + + # Tests needs a known configuration of node/scope/compiler since it parses and evaluates + # snippets as the compiler will evaluate them, butwithout the overhead of compiling a complete + # catalog for each tested expression. + # + @parser = Puppet::Pops::Parser::EvaluatingParser::Transitional.new + @node = Puppet::Node.new('node.example.com') + @node.environment = Puppet::Node::Environment.create(:testing, []) + @compiler = Puppet::Parser::Compiler.new(@node) + @scope = Puppet::Parser::Scope.new(@compiler) + @scope.source = Puppet::Resource::Type.new(:node, 'node.example.com') + @scope.parent = @compiler.topscope end - let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } - let(:node) { 'node.example.com' } - let(:scope) { s = create_test_scope_for_node(node); s } + let(:parser) { @parser } + let(:scope) { @scope } types = Puppet::Pops::Types::TypeFactory + def create_test_scope_for_node(node_name) + end + context "When evaluator evaluates literals" do { "1" => 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do # This test must be done with match_array since the order of the hash # is undefined and Ruby 1.8.7 and 1.9.3 produce different results. expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result) end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "'1' < 1.1" => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "1.0 < 'a'" => true, "'1.0' < 1.1" => true, "'1.0' < 'a'" => true, "'1.0' < '' " => true, "'1.0' < ' '" => true, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, "a =~ Regexp['a.*']" => false, # String is not subtype of Regexp. PUP-957 "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "String[1] in bananas" => false, # Philosophically true though :-) "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => true, "'15' in [1, 0xf]" => true, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Object' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, "$a = [1,2,3]; [x].map |$x| { $a += x; $a }" => [[1,2,3,'x']], "$a = [a,x,c]; [x].map |$x| { $a -= x; $a }" => [['a','c']], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]; $a == 1 and $b == 2 and $c == 3" => :error, "[a,b,c] = {b=>2,c=>3,a=>1}; $a == 1 and $b == 2 and $c == 3" => :error, "$a = [1,2,3]; [x].collect |$x| { [a] += x; $a }" => :error, "$a = [a,x,c]; [x].collect |$x| { [a] -= x; $a }" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', "case 1 { Integer : { yes } Type[Integer] : { no } }" => 'yes', "case Integer { Integer : { no } Type[Integer] : { yes } }" => 'yes', # supports unfold "case ringo { *[paul, john, ringo, george] : { 'beatle' } }" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'no', "3 ? { 1 => no, 3 => no, default => yes }" => 'no', "4 ? { 1 => no, default => yes, 3 => no }" => 'yes', "4 ? { 1 => no, 3 => no, default => yes }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', "ringo ? *[paul, john, ringo, george] => 'beatle'" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it 'fails if a selector does not match' do expect{parser.evaluate_string(scope, "2 ? 3 => 4")}.to raise_error(/No matching entry for selector parameter with value '2'/) end end context "When evaluator evaluated unfold" do { "*[1,2,3]" => [1,2,3], "*1" => [1], "*'a'" => ['a'] }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate the expression '*{a=>10, b=>20} to [['a',10],['b',20]]" do result = parser.evaluate_string(scope, '*{a=>10, b=>20}', __FILE__) expect(result).to include(['a', 10]) expect(result).to include(['b', 20]) end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], "[1,*[2,3],4]" => [1,2,3,4], "[1,*[2,3],4][1]" => 2, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works', "$a = {undef => 10} $a[free_lunch]" => nil, "$a = {undef => 10} $a[undef]" => 10, "$a = {undef => 10} $a[$a[free_lunch]]" => 10, "$a = {} $a[free_lunch] == undef" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default), "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'), "String[1,2]" => types.constrain_size(types.string,1, 2), "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2), "String[Integer[1]]" => types.constrain_size(types.string,1, :default), }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "1[]" => :error, "3.14[]" => :error, "/.*/[]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/) end end # Errors when wrong number/type of keys are used { "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum', "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4', "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type", "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5', "'abc'[x]" => "The value 'x' cannot be converted to Numeric", "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected", "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3", "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Fixnum', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Fixnum where String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Fixnum where String is expected', "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String", "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Fixnum where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Fixnum where String or Regexp is expected', "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2', "true[0]" => "Operator '[]' is not applicable to a Boolean", "1[0]" => "Operator '[]' is not applicable to an Integer", "3.14[0]" => "Operator '[]' is not applicable to a Float", "/.*/[0]" => "Operator '[]' is not applicable to a Regexp", "[1][a]" => "The value 'a' cannot be converted to Numeric", "[1][0.0]" => "An Array[] cannot use Float where Integer is expected", "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result))) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) Puppet.override({:global_scope => scope}, "test") do scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end end # Resource default and override expressions and resource parameter access with [] { # Properties "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", # Parameters "notify { id: withpath=>explicit} Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } notify { foo: } Notify[foo][withpath]" => "by_default", "notify {foo:} Notify[foo]{withpath=>by_override} Notify[foo][withpath]" => "by_override", # Metaparameters "notify { foo: tag => evoe} Notify[foo][tag]" => "evoe", # Does not produce the defaults for tag parameter (title, type or names of scopes) "notify { foo: } Notify[foo][tag]" => nil, # But a default may be specified on the type "Notify { tag=>by_default } notify { foo: } Notify[foo][tag]" => "by_default", "Notify { tag=>by_default } notify { foo: } Notify[foo]{ tag=>by_override } Notify[foo][tag]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Virtual and realized resource default and overridden resource parameter access with [] { # Properties "@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@notify { id: message=>explicit } realize Notify[id] Notify[id][message]" => "explicit", "Notify { message=>by_default } @notify { id: } Notify[id][message]" => "by_default", "Notify { message=>by_default } @notify { id: tag=>thisone } Notify <| tag == thisone |>; Notify[id][message]" => "by_default", "@notify { id: } Notify[id]{message=>by_override} Notify[id][message]" => "by_override", # Parameters "@notify { id: withpath=>explicit } Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } @notify { id: } Notify[id][withpath]" => "by_default", "@notify { id: } realize Notify[id] Notify[id]{withpath=>by_override} Notify[id][withpath]" => "by_override", # Metaparameters "@notify { id: tag=>explicit } Notify[id][tag]" => "explicit", }.each do |source, result| it "parses and evaluates virtual and realized resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Exported resource attributes { "@@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@@notify { id: message=>explicit, tag=>thisone } Notify <<| tag == thisone |>> Notify[id][message]" => "explicit", }.each do |source, result| it "parses and evaluates exported resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, # NOTE: these meta-esque parameters are not recognized as such "notify { id: message=>explicit} Notify[id][title]" => /does not have a parameter called 'title'/, "notify { id: message=>explicit} Notify[id]['type']" => /does not have a parameter called 'type'/, "notify { id: message=>explicit } Notify[id]{message=>override}" => /'message' is already set on Notify\[id\]/ }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end context 'with errors' do { "Class['fail-whale']" => /Illegal name/, "Class[0]" => /An Integer cannot be used where a String is expected/, "Class[/.*/]" => /A Regexp cannot be used where a String is expected/, "Class[4.1415]" => /A Float cannot be used where a String is expected/, "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/, "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/, }.each do | source, error_pattern| it "an error is flagged for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern) end end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => false, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs operations on literal undef" do it "computes non existing hash lookup as undef" do parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true end end context "When evaluator performs calls" do - around(:each) do |example| - Puppet.override(:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))) do - example.run - end - end let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", # unfolds 'sprintf( *["x%iy", $a] )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end it "provides location information on error in unparenthesized call logic" do expect{parser.evaluate_string(scope, "include non_existing_class", __FILE__)}.to raise_error(Puppet::ParseError, /line 1\:1/) end it 'defaults can be given in a lambda and used only when arg is missing' do - env_loader = Puppet.lookup(:loaders).public_environment_loader + env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Integer', 'count' required_block_param end def test(count, block) block.call({}, *[].fill(10, 0, count)) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(1) |$x, $y=20| { $x + $y}")).to eql(30) expect(parser.evaluate_string(scope, "test(2) |$x, $y=20| { $x + $y}")).to eql(20) end it 'a given undef does not select the default value' do - env_loader = Puppet.lookup(:loaders).public_environment_loader + env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Object', 'lambda_arg' required_block_param end def test(lambda_arg, block) block.call({}, lambda_arg) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(undef) |$x=20| { $x == undef}")).to eql(true) end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end it 'should accept a variable with leading underscore when used directly' do source = '$_x = 10; "$_x"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end it 'should accept a variable with leading underscore when used as an expression' do source = '$_x = 10; "${_x}"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end it "a lex error should be raised for '$foo::::bar'" do expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/) end { '$a = $0' => nil, '$a = $1' => nil, }.each do |source, value| it "it is ok to reference numeric unassigned variables '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end { '$00 = 0' => /must be a decimal value/, '$0xf = 0' => /must be a decimal value/, '$0777 = 0' => /must be a decimal value/, '$123a = 0' => /must be a decimal value/, }.each do |source, error_pattern| it "should raise an error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern) end end context "an initial underscore in the last segment of a var name is allowed" do { '$_a = 1' => 1, '$__a = 1' => 1, }.each do |source, value| it "as in this example '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end end end context "When evaluating relationships" do it 'should form a relation with File[a] -> File[b]' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with resource -> resource' do source = "notify{a:} -> notify{b:}" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b']) end it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do source = "[File[a], File[b]] -> [File[x], File[y]]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y']) end it 'should tolerate (eliminate) duplicates in operands' do source = "[File[a], File[a]] -> File[x]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.relationships.size.should == 1 end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end context "When evaluating heredoc" do it "evaluates plain heredoc" do src = "@(END)\nThis is\nheredoc text\nEND\n" parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin" do src = [ "@(END)", " This is", " heredoc text", " | END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin and right newline trim" do src = [ "@(END)", " This is", " heredoc text", " |- END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text" end it "parses escape specification" do src = <<-CODE @(END/t) Tex\\tt\\n |- END CODE parser.evaluate_string(scope, src).should == "Tex\tt\\n" end it "parses syntax checked specification" do src = <<-CODE @(END:json) ["foo", "bar"] |- END CODE parser.evaluate_string(scope, src).should == '["foo", "bar"]' end it "parses syntax checked specification with error and reports it" do src = <<-CODE @(END:json) ['foo', "bar"] |- END CODE expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/) end it "parses interpolated heredoc expression" do src = <<-CODE $name = 'Fjodor' @("END") Hello $name |- END CODE parser.evaluate_string(scope, src).should == "Hello Fjodor" end end context "Handles Deprecations and Discontinuations" do - around(:each) do |example| - Puppet.override({:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}, 'test') do - example.run - end - end - it 'of import statements' do source = "\nimport foo" # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error(/'import' has been discontinued.*line 2:1/) end end context "Detailed Error messages are reported" do it 'for illegal type references' do source = '1+1 { "title": }' # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.parse_string(source, nil) }.to raise_error(/Expression is not valid as a resource.*line 1:5/) end it 'for non r-value producing <| |>' do expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'for non r-value producing <<| |>>' do expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'for non r-value producing define' do Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/) end it 'for non r-value producing class' do Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/) end it 'for unclosed quote with indication of start position of string' do source = <<-SOURCE.gsub(/^ {6}/,'') $a = "xx yyy SOURCE # first char after opening " reported as being in error. expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/) end it 'for multiple errors with a summary exception' do Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/) end it 'for a bad hostname' do expect { parser.parse_string("node 'macbook+owned+by+name' { }", nil) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/) end it 'for a hostname with interpolation' do source = <<-SOURCE.gsub(/^ {6}/,'') $name = 'fred' node "macbook-owned-by$name" { } SOURCE expect { parser.parse_string(source, nil) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/) end end matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end