diff --git a/lib/puppet/functions/each.rb b/lib/puppet/functions/each.rb index 1b9ac937c..dbdf1b614 100644 --- a/lib/puppet/functions/each.rb +++ b/lib/puppet/functions/each.rb @@ -1,111 +1,111 @@ # 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_2 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[2,2]', :block end dispatch :foreach_Hash_1 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[1,1]', :block end dispatch :foreach_Enumerable_2 do param 'Any', :enumerable required_block_param 'Callable[2,2]', :block end dispatch :foreach_Enumerable_1 do param 'Any', :enumerable required_block_param 'Callable[1,1]', :block end def foreach_Hash_1(hash, pblock) enumerator = hash.each_pair hash.size.times do - pblock.call(nil, enumerator.next) + pblock.call(enumerator.next) end # produces the receiver hash end def foreach_Hash_2(hash, pblock) enumerator = hash.each_pair hash.size.times do - pblock.call(nil, *enumerator.next) + pblock.call(*enumerator.next) end # produces the receiver hash end def foreach_Enumerable_1(enumerable, pblock) enum = asserted_enumerable(enumerable) begin - loop { pblock.call(nil, enum.next) } + loop { pblock.call(enum.next) } rescue StopIteration end # produces the receiver enumerable end def foreach_Enumerable_2(enumerable, pblock) enum = asserted_enumerable(enumerable) index = 0 begin loop do - pblock.call(nil, index, enum.next) + pblock.call(index, enum.next) index += 1 end rescue StopIteration end # produces the receiver enumerable 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 diff --git a/lib/puppet/functions/filter.rb b/lib/puppet/functions/filter.rb index 0654d9c9c..c88909d32 100644 --- a/lib/puppet/functions/filter.rb +++ b/lib/puppet/functions/filter.rb @@ -1,113 +1,113 @@ # 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_2 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[2,2]', :block end dispatch :filter_Hash_1 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[1,1]', :block end dispatch :filter_Enumerable_2 do param 'Any', :enumerable required_block_param 'Callable[2,2]', :block end dispatch :filter_Enumerable_1 do param 'Any', :enumerable required_block_param 'Callable[1,1]', :block end def filter_Hash_1(hash, pblock) - result = hash.select {|x, y| pblock.call(self, [x, y]) } + result = hash.select {|x, y| pblock.call([x, y]) } # Ruby 1.8.7 returns Array result = Hash[result] unless result.is_a? Hash result end def filter_Hash_2(hash, pblock) - result = hash.select {|x, y| pblock.call(self, x, y) } + result = hash.select {|x, y| pblock.call(x, y) } # Ruby 1.8.7 returns Array result = Hash[result] unless result.is_a? Hash result end def filter_Enumerable_1(enumerable, pblock) result = [] index = 0 enum = asserted_enumerable(enumerable) begin loop do it = enum.next - if pblock.call(nil, it) == true + if pblock.call(it) == true result << it end end rescue StopIteration end result end def filter_Enumerable_2(enumerable, pblock) result = [] index = 0 enum = asserted_enumerable(enumerable) begin loop do it = enum.next - if pblock.call(nil, index, it) == true + if pblock.call(index, it) == true result << it end index += 1 end rescue StopIteration end result 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 diff --git a/lib/puppet/functions/map.rb b/lib/puppet/functions/map.rb index 2141d1e81..c0f049aec 100644 --- a/lib/puppet/functions/map.rb +++ b/lib/puppet/functions/map.rb @@ -1,97 +1,97 @@ # 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_2 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[2,2]', :block end dispatch :map_Hash_1 do param 'Hash[Any, Any]', :hash required_block_param 'Callable[1,1]', :block end dispatch :map_Enumerable_2 do param 'Any', :enumerable required_block_param 'Callable[2,2]', :block end dispatch :map_Enumerable_1 do param 'Any', :enumerable required_block_param 'Callable[1,1]', :block end def map_Hash_1(hash, pblock) - hash.map {|x, y| pblock.call(nil, [x, y]) } + hash.map {|x, y| pblock.call([x, y]) } end def map_Hash_2(hash, pblock) - hash.map {|x, y| pblock.call(nil, x, y) } + hash.map {|x, y| pblock.call(x, y) } end def map_Enumerable_1(enumerable, pblock) result = [] index = 0 enum = asserted_enumerable(enumerable) begin - loop { result << pblock.call(nil, enum.next) } + loop { result << pblock.call(enum.next) } rescue StopIteration end result end def map_Enumerable_2(enumerable, pblock) result = [] index = 0 enum = asserted_enumerable(enumerable) begin loop do - result << pblock.call(nil, index, enum.next) + result << pblock.call(index, enum.next) index = index +1 end rescue StopIteration end result 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 diff --git a/lib/puppet/functions/reduce.rb b/lib/puppet/functions/reduce.rb index 5b54e41c5..f5de76b4b 100644 --- a/lib/puppet/functions/reduce.rb +++ b/lib/puppet/functions/reduce.rb @@ -1,94 +1,94 @@ # 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 'Any', :enumerable required_block_param 'Callable[2,2]', :block end dispatch :reduce_with_memo do param 'Any', :enumerable param 'Any', :memo required_block_param 'Callable[2,2]', :block end def reduce_without_memo(enumerable, pblock) enum = asserted_enumerable(enumerable) - enum.reduce {|memo, x| pblock.call(nil, memo, x) } + enum.reduce {|memo, x| pblock.call(memo, x) } end def reduce_with_memo(enumerable, given_memo, pblock) enum = asserted_enumerable(enumerable) - enum.reduce(given_memo) {|memo, x| pblock.call(nil, memo, x) } + enum.reduce(given_memo) {|memo, x| pblock.call(memo, x) } 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 diff --git a/lib/puppet/functions/scanf.rb b/lib/puppet/functions/scanf.rb index f107c68a8..13e4a51a7 100644 --- a/lib/puppet/functions/scanf.rb +++ b/lib/puppet/functions/scanf.rb @@ -1,46 +1,46 @@ # Scans a string and returns an array of one or more converted values as directed by a given format string.args # See the documenation of Ruby's String::scanf method for details about the supported formats (which # are similar but not identical to the formats used in Puppet's `sprintf` function. # # This function takes two mandatory arguments: the first is the String to convert, and the second # the format String. A parameterized block may optionally be given, which is called with the result # that is produced by scanf if no block is present, the result of the block is then returned by # the function. # # The result of the scan is an Array, with each sucessfully scanned and transformed value.args The scanning # stops if a scan is unsuccesful and the scanned result up to that point is returned. If there was no # succesful scan at all, the result is an empty Array. The optional code block is typically used to # assert that the scan was succesful, and either produce the same input, or perform unwrapping of # the result # # @example scanning an integer in string form (result is an array with # integer, or empty if unsuccessful) # "42".scanf("%i") # # @example scanning and processing resulting array to assert result and unwrap # # "42".scanf("%i") |$x| { # unless $x[0] =~ Integer { # fail "Expected a well formed integer value, got '$x[0]'" # } # $x[0] # } # # @since 3.7.4 Puppet::Functions.create_function(:scanf) do require 'scanf' dispatch :scanf do param 'String', 'data' param 'String', 'format' optional_block_param end def scanf(data, format, block=nil) result = data.scanf(format) if !block.nil? - result = block.call({}, result) + result = block.call(result) end result end end diff --git a/lib/puppet/functions/slice.rb b/lib/puppet/functions/slice.rb index ef3a2932a..6c83b5637 100644 --- a/lib/puppet/functions/slice.rb +++ b/lib/puppet/functions/slice.rb @@ -1,126 +1,126 @@ # 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[Any, Any]', :hash param 'Integer[1, default]', :slize_size optional_block_param end dispatch :slice_Enumerable do param 'Any', :enumerable param 'Integer[1, default]', :slize_size optional_block_param end 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, nil, 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) + pblock.call(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) + pblock.call(*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.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 serving_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 diff --git a/lib/puppet/functions/with.rb b/lib/puppet/functions/with.rb index 6dd51ec18..9971fa6b3 100644 --- a/lib/puppet/functions/with.rb +++ b/lib/puppet/functions/with.rb @@ -1,23 +1,23 @@ # Call a lambda with the given arguments. Since the parameters of the lambda # are local to the lambda's scope, this can be used to create private sections # of logic in a class so that the variables are not visible outside of the # class. # # @example Using with # # # notices the array [1, 2, 'foo'] # with(1, 2, 'foo') |$x, $y, $z| { notice [$x, $y, $z] } # # @since 3.7.0 # Puppet::Functions.create_function(:with) do dispatch :with do param 'Any', 'arg' arg_count(0, :default) required_block_param end def with(*args) - args[-1].call({}, *args[0..-2]) + args[-1].call(*args[0..-2]) end end diff --git a/lib/puppet/pops/binder/lookup.rb b/lib/puppet/pops/binder/lookup.rb index 07980ce62..2c45923c3 100644 --- a/lib/puppet/pops/binder/lookup.rb +++ b/lib/puppet/pops/binder/lookup.rb @@ -1,199 +1,199 @@ # This class is the backing implementation of the Puppet function 'lookup'. # See puppet/parser/functions/lookup.rb for documentation. # class Puppet::Pops::Binder::Lookup def self.parse_lookup_args(args) options = {} pblock = if args[-1].respond_to?(:puppet_lambda) args.pop end case args.size when 1 # name, or all options if args[ 0 ].is_a?(Hash) options = to_symbolic_hash(args[ 0 ]) else options[ :name ] = args[ 0 ] end when 2 # name and type, or name and options if args[ 1 ].is_a?(Hash) options = to_symbolic_hash(args[ 1 ]) options[:name] = args[ 0 ] # silently overwrite option with given name else options[:name] = args[ 0 ] options[:type] = args[ 1 ] end when 3 # name, type, default (no options) options[ :name ] = args[ 0 ] options[ :type ] = args[ 1 ] options[ :default ] = args[ 2 ] else raise Puppet::ParseError, "The lookup function accepts 1-3 arguments, got #{args.size}" end options[:pblock] = pblock options end def self.to_symbolic_hash(input) names = [:name, :type, :default, :accept_undef, :extra, :override] options = {} names.each {|n| options[n] = undef_as_nil(input[n.to_s] || input[n]) } options end def self.type_mismatch(type_calculator, expected, got) "has wrong type, expected #{type_calculator.string(expected)}, got #{type_calculator.string(got)}" end def self.fail(msg) raise Puppet::ParseError, "Function lookup() " + msg end def self.fail_lookup(names) name_part = if names.size == 1 "the name '#{names[0]}'" else "any of the names ['" + names.join(', ') + "']" end fail("did not find a value for #{name_part}") end def self.validate_options(options, type_calculator) type_parser = Puppet::Pops::Types::TypeParser.new name_type = type_parser.parse('Variant[Array[String], String]') if is_nil_or_undef?(options[:name]) || options[:name].is_a?(Array) && options[:name].empty? fail ("requires a name, or array of names. Got nothing to lookup.") end t = type_calculator.infer(options[:name]) if ! type_calculator.assignable?(name_type, t) fail("given 'name' argument, #{type_mismatch(type_calculator, options[:name], t)}") end # unless a type is already given (future case), parse the type (or default 'Data'), fails if invalid type is given unless options[:type].is_a?(Puppet::Pops::Types::PAnyType) options[:type] = type_parser.parse(options[:type] || 'Data') end # default value must comply with the given type if options[:default] t = type_calculator.infer(options[:default]) if ! type_calculator.assignable?(options[:type], t) fail("'default' value #{type_mismatch(type_calculator, options[:type], t)}") end end if options[:extra] && !options[:extra].is_a?(Hash) # do not perform inference here, it is enough to know that it is not a hash fail("'extra' value must be a Hash, got #{options[:extra].class}") end options[:extra] = {} unless options[:extra] if options[:override] && !options[:override].is_a?(Hash) # do not perform inference here, it is enough to know that it is not a hash fail("'override' value must be a Hash, got #{options[:extra].class}") end options[:override] = {} unless options[:override] end def self.nil_as_undef(x) x.nil? ? :undef : x end def self.undef_as_nil(x) is_nil_or_undef?(x) ? nil : x end def self.is_nil_or_undef?(x) x.nil? || x == :undef end # This is used as a marker - a value that cannot (at least not easily) by mistake be found in # hiera data. # class PrivateNotFoundMarker; end def self.search_for(scope, type, name, options) # search in order, override, injector, hiera, then extra if !(result = options[:override][name]).nil? result elsif !(result = scope.compiler.injector.lookup(scope, type, name)).nil? result else result = scope.function_hiera([name, PrivateNotFoundMarker]) if !result.nil? && result != PrivateNotFoundMarker result else options[:extra][name] end end end # This is the method called from the puppet/parser/functions/lookup.rb # @param args [Array] array following the puppet function call conventions def self.lookup(scope, args) type_calculator = Puppet::Pops::Types::TypeCalculator.new options = parse_lookup_args(args) validate_options(options, type_calculator) names = [options[:name]].flatten type = options[:type] result_with_name = names.reduce([]) do |memo, name| break memo if !memo[1].nil? [name, search_for(scope, type, name, options)] end result = if result_with_name[1].nil? # not found, use default (which may be nil), the default is already type checked options[:default] else # injector.lookup is type-safe already do no need to type check the result result_with_name[1] end # If a block is given it is called with :undef passed as 'nil' since the lookup function # is available from 3x with --binder turned on, and the evaluation is always 4x. # TODO PUPPET4: Simply pass the value # result = if pblock = options[:pblock] result2 = case pblock.parameter_count when 1 - pblock.call(scope, undef_as_nil(result)) + pblock.call(undef_as_nil(result)) when 2 - pblock.call(scope, result_with_name[ 0 ], undef_as_nil(result)) + pblock.call(result_with_name[ 0 ], undef_as_nil(result)) else - pblock.call(scope, result_with_name[ 0 ], undef_as_nil(result), undef_as_nil(options[ :default ])) + pblock.call(result_with_name[ 0 ], undef_as_nil(result), undef_as_nil(options[ :default ])) end # if the given result was returned, there is no need to type-check it again if !result2.equal?(result) t = type_calculator.infer(undef_as_nil(result2)) if !type_calculator.assignable?(type, t) fail "the value produced by the given code block #{type_mismatch(type_calculator, type, t)}" end end result2 else result end # Finally, the result if nil must be acceptable or an error is raised if is_nil_or_undef?(result) && !options[:accept_undef] fail_lookup(names) else # Since the function may be used without future parser being in effect, nil is not handled in a good # way, and should instead be turned into :undef. # TODO PUPPET4: Simply return the result # Puppet[:parser] == 'future' ? result : nil_as_undef(result) end end end diff --git a/lib/puppet/pops/binder/producers.rb b/lib/puppet/pops/binder/producers.rb index 8302ebf4c..53a74e477 100644 --- a/lib/puppet/pops/binder/producers.rb +++ b/lib/puppet/pops/binder/producers.rb @@ -1,826 +1,826 @@ # This module contains the various producers used by Puppet Bindings. # The main (abstract) class is {Puppet::Pops::Binder::Producers::Producer} which documents the # Producer API and serves as a base class for all other producers. # It is required that custom producers inherit from this producer (directly or indirectly). # # The selection of a Producer is typically performed by the Innjector when it configures itself # from a Bindings model where a {Puppet::Pops::Binder::Bindings::ProducerDescriptor} describes # which producer to use. The configuration uses this to create the concrete producer. # It is possible to describe that a particular producer class is to be used, and also to describe that # a custom producer (derived from Producer) should be used. This is available for both regular # bindings as well as multi-bindings. # # # @api public # module Puppet::Pops::Binder::Producers # Producer is an abstract base class representing the base contract for a bound producer. # Typically, when a lookup is performed it is the value that is returned (via a producer), but # it is also possible to lookup the producer, and ask it to produce the value (the producer may # return a series of values, which makes this especially useful). # # When looking up a producer, it is of importance to only use the API of the Producer class # unless it is known that a particular custom producer class has been bound. # # Custom Producers # ---------------- # The intent is that this class is derived for custom producers that require additional # options/arguments when producing an instance. Such a custom producer may raise an error if called # with too few arguments, or may implement specific `produce` methods and always raise an # error on #produce indicating that this producer requires custom calls and that it can not # be used as an implicit producer. # # Features of Producer # -------------------- # The Producer class is abstract, but offers the ability to transform the produced result # by passing the option `:transformer` which should be a Puppet Lambda Expression taking one argument # and producing the transformed (wanted) result. # # @abstract # @api public # class Producer # A Puppet 3 AST Lambda Expression # @api public # attr_reader :transformer # Creates a Producer. # Derived classes should call this constructor to get support for transformer lambda. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) if transformer_lambda = options[:transformer] if transformer_lambda.is_a?(Proc) - raise ArgumentError, "Transformer Proc must take two arguments; scope, value." unless transformer_lambda.arity == 2 + raise ArgumentError, "Transformer Proc must take one argument; value." unless transformer_lambda.arity == 1 @transformer = transformer_lambda else raise ArgumentError, "Transformer must be a LambdaExpression" unless transformer_lambda.is_a?(Puppet::Pops::Model::LambdaExpression) raise ArgumentError, "Transformer lambda must take one argument; value." unless transformer_lambda.parameters.size() == 1 @transformer = Puppet::Pops::Parser::EvaluatingParser.new.closure(transformer_lambda, scope) end end end # Produces an instance. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @param args [Object] arguments to custom producers, always empty for implicit productions # @return [Object] the produced instance (should never be nil). # @api public # def produce(scope, *args) do_transformation(scope, internal_produce(scope)) end # Returns the producer after possibly having recreated an internal/wrapped producer. # This implementation returns `self`. A derived class may want to override this method # to perform initialization/refresh of its internal state. This method is called when # a producer is requested. # @see Puppet::Pops::Binder::ProducerProducer for an example of implementation. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @return [Puppet::Pops::Binder::Producer] the producer to use # @api public # def producer(scope) self end protected # Derived classes should implement this method to do the production of a value # @param scope [Puppet::Parser::Scope] the scope to use when performing lookup and evaluation # @raise [NotImplementedError] this implementation always raises an error # @abstract # @api private # def internal_produce(scope) raise NotImplementedError, "Producer-class '#{self.class.name}' should implement #internal_produce(scope)" end # Transforms the produced value if a transformer has been defined. # @param scope [Puppet::Parser::Scope] the scope used for evaluation # @param produced_value [Object, nil] the produced value (possibly nil) # @return [Object] the transformed value if a transformer is defined, else the given `produced_value` # @api private # def do_transformation(scope, produced_value) return produced_value unless transformer - transformer.call(scope, produced_value) + transformer.call(produced_value) end end # Abstract Producer holding a value # @abstract # @api public # class AbstractValueProducer < Producer # @api public attr_reader :value # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression, nil] :value (nil) the value to produce # @api public # def initialize(injector, binding, scope, options) super # nil is ok here, as an abstract value producer may be used to signal "not found" @value = options[:value] end end # Produces the same/singleton value on each production # @api public # class SingletonProducer < AbstractValueProducer protected # @api private def internal_produce(scope) value() end end # Produces a deep clone of its value on each production. # @api public # class DeepCloningProducer < AbstractValueProducer protected # @api private def internal_produce(scope) case value when Integer, Float, TrueClass, FalseClass, Symbol # These are immutable return value when String # ok if frozen, else fall through to default return value() if value.frozen? end # The default: serialize/deserialize to get a deep copy Marshal.load(Marshal.dump(value())) end end # This abstract producer class remembers the injector and binding. # @abstract # @api public # class AbstractArgumentedProducer < Producer # @api public attr_reader :injector # @api public attr_reader :binding # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) super @injector = injector @binding = binding end end # @api public class InstantiatingProducer < AbstractArgumentedProducer # @api public attr_reader :the_class # @api public attr_reader :init_args # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [String] :class_name The name of the class to create instance of # @option options [Array] :init_args ([]) Optional arguments to class constructor # @api public # def initialize(injector, binding, scope, options) # Better do this, even if a transformation of a created instance is kind of an odd thing to do, one can imagine # sending it to a function for further detailing. # super class_name = options[:class_name] raise ArgumentError, "Option 'class_name' must be given for an InstantiatingProducer" unless class_name # get class by name @the_class = Puppet::Pops::Types::ClassLoader.provide(class_name) @init_args = options[:init_args] || [] raise ArgumentError, "Can not load the class #{class_name} specified in binding named: '#{binding.name}'" unless @the_class end protected # Performs initialization the same way as Assisted Inject does (but handle arguments to # constructor) # @api private # def internal_produce(scope) result = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific # constructor exists. (i.e do not pick :inject from superclass if class has a constructor). # if the_class.respond_to?(:inject) inject_method = the_class.method(:inject) initialize_method = the_class.instance_method(:initialize) if inject_method.owner <= initialize_method.owner result = the_class.inject(injector, scope, binding, *init_args) end end if result.nil? result = the_class.new(*init_args) end result end end # @api public class FirstFoundProducer < Producer # @api public attr_reader :producers # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :producers list of producers to consult. Required. # @api public # def initialize(injector, binding, scope, options) super @producers = options[:producers] raise ArgumentError, "Option :producers' must be set to a list of producers." if @producers.nil? raise ArgumentError, "Given 'producers' option is not an Array" unless @producers.is_a?(Array) end protected # @api private def internal_produce(scope) # return the first produced value that is non-nil (unfortunately there is no such enumerable method) producers.reduce(nil) {|memo, p| break memo unless memo.nil?; p.produce(scope)} end end # Evaluates a Puppet Expression and returns the result. # This is typically used for strings with interpolated expressions. # @api public # class EvaluatingProducer < Producer # A Puppet 3 AST Expression # @api public # attr_reader :expression # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :expression The expression to evaluate # @api public # def initialize(injector, binding, scope, options) super @expression = options[:expression] raise ArgumentError, "Option 'expression' must be given to an EvaluatingProducer." unless @expression end # @api private def internal_produce(scope) Puppet::Pops::Parser::EvaluatingParser.new.evaluate(scope, expression) end end # @api public class LookupProducer < AbstractArgumentedProducer # @api public attr_reader :type # @api public attr_reader :name # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binder [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PAnyType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @api public # def initialize(injector, binder, scope, options) super @type = options[:type] @name = options[:name] || '' raise ArgumentError, "Option 'type' must be given in a LookupProducer." unless @type end protected # @api private def internal_produce(scope) injector.lookup_type(scope, type, name) end end # @api public class LookupKeyProducer < LookupProducer # @api public attr_reader :key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binder [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PAnyType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @option options [Puppet::Pops::Types::PAnyType] :key The key to lookup in the hash # @api public # def initialize(injector, binder, scope, options) super @key = options[:key] raise ArgumentError, "Option 'key' must be given in a LookupKeyProducer." if key.nil? end protected # @api private def internal_produce(scope) result = super result.is_a?(Hash) ? result[key] : nil end end # Produces the given producer, then uses that producer. # @see ProducerProducer for the non singleton version # @api public # class SingletonProducerProducer < Producer # @api public attr_reader :value_producer # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression] :producer_producer a producer of a value producer (required) # @api public # def initialize(injector, binding, scope, options) super p = options[:producer_producer] raise ArgumentError, "Option :producer_producer must be given in a SingletonProducerProducer" unless p @value_producer = p.produce(scope) end protected # @api private def internal_produce(scope) value_producer.produce(scope) end end # A ProducerProducer creates a producer via another producer, and then uses this created producer # to produce values. This is useful for custom production of series of values. # On each request for a producer, this producer will reset its internal producer (i.e. restarting # the series). # # @param producer_producer [#produce(scope)] the producer of the producer # # @api public # class ProducerProducer < Producer # @api public attr_reader :producer_producer # @api public attr_reader :value_producer # Creates new ProducerProducer given a producer. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Binder::Producer] :producer_producer a producer of a value producer (required) # # @api public # def initialize(injector, binding, scope, options) super unless producer_producer = options[:producer_producer] raise ArgumentError, "The option :producer_producer must be set in a ProducerProducer" end raise ArgumentError, "Argument must be a Producer" unless producer_producer.is_a?(Producer) @producer_producer = producer_producer @value_producer = nil end # Updates the internal state to use a new instance of the wrapped producer. # @api public # def producer(scope) @value_producer = @producer_producer.produce(scope) self end protected # Produces a value after having created an instance of the wrapped producer (if not already created). # @api private # def internal_produce(scope, *args) producer() unless value_producer value_producer.produce(scope) end end # This type of producer should only be created by the Injector. # # @api private # class AssistedInjectProducer < Producer # An Assisted Inject Producer is created when a lookup is made of a type that is # not bound. It does not support a transformer lambda. # @note This initializer has a different signature than all others. Do not use in regular logic. # @api private # def initialize(injector, clazz) raise ArgumentError, "class must be given" unless clazz.is_a?(Class) @injector = injector @clazz = clazz @inst = nil end def produce(scope, *args) producer(scope, *args) unless @inst @inst end # @api private def producer(scope, *args) @inst = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific zero args # constructor exists. (i.e do not pick :inject from superclass if class has a zero args constructor). # if @clazz.respond_to?(:inject) inject_method = @clazz.method(:inject) initialize_method = @clazz.instance_method(:initialize) if inject_method.owner <= initialize_method.owner || initialize_method.arity != 0 @inst = @clazz.inject(@injector, scope, nil, *args) end end if @inst.nil? unless args.empty? raise ArgumentError, "Assisted Inject can not pass arguments to no-args constructor when there is no class inject method." end @inst = @clazz.new() end self end end # Abstract base class for multibind producers. # Is suitable as base class for custom implementations of multibind producers. # @abstract # @api public # class MultibindProducer < AbstractArgumentedProducer attr_reader :contributions_key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # # @api public # def initialize(injector, binding, scope, options) super @contributions_key = injector.key_factory.multibind_contributions(binding.id) end # @param expected [Array, Puppet::Pops::Types::PAnyType] expected type or types # @param actual [Object, Puppet::Pops::Types::PAnyType> the actual value (or its type) # @return [String] a formatted string for inclusion as detail in an error message # @api private # def type_error_detail(expected, actual) tc = injector.type_calculator expected = [expected] unless expected.is_a?(Array) actual_t = tc.is_ptype?(actual) ? actual : tc.infer(actual) expstrs = expected.collect {|t| tc.string(t) } "expected: #{expstrs.join(', or ')}, got: #{tc.string(actual_t)}" end end # A configurable multibind producer for Array type multibindings. # # This implementation collects all contributions to the multibind and then combines them using the following rules: # # - all *unnamed* entries are added unless the option `:priority_on_unnamed` is set to true, in which case the unnamed # contribution with the highest priority is added, and the rest are ignored (unless they have the same priority in which # case an error is raised). # - all *named* entries are handled the same way as *unnamed* but the option `:priority_on_named` controls their handling. # - the option `:uniq` post processes the result to only contain unique entries # - the option `:flatten` post processes the result by flattening all nested arrays. # - If both `:flatten` and `:uniq` are true, flattening is done first. # # @note # Collection accepts elements that comply with the array's element type, or the entire type (i.e. Array[element_type]). # If the type is restrictive - e.g. Array[String] and an Array[String] is contributed, the result will not be type # compliant without also using the `:flatten` option, and a type error will be raised. For an array with relaxed typing # i.e. Array[Data], it is valid to produce a result such as `['a', ['b', 'c'], 'd']` and no flattening is required # and no error is raised (but using the array needs to be aware of potential array, non-array entries. # The use of the option `:flatten` controls how the result is flattened. # # @api public # class ArrayMultibindProducer < MultibindProducer # @return [Boolean] whether the result should be made contain unique (non-equal) entries or not # @api public attr_reader :uniq # @return [Boolean, Integer] If result should be flattened (true), or not (false), or flattened to given level (0 = none, -1 = all) # @api public attr_reader :flatten # @return [Boolean] whether priority should be considered for named contributions # @api public attr_reader :priority_on_named # @return [Boolean] whether priority should be considered for unnamed contributions # @api public attr_reader :priority_on_unnamed # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Boolean] :uniq (false) if collected result should be post-processed to contain only unique entries # @option options [Boolean, Integer] :flatten (false) if collected result should be post-processed so all contained arrays # are flattened. May be set to an Integer value to indicate the level of recursion (-1 is endless, 0 is none). # @option options [Boolean] :priority_on_named (true) if highest precedented named element should win or if all should be included # @option options [Boolean] :priority_on_unnamed (false) if highest precedented unnamed element should win or if all should be included # @api public # def initialize(injector, binding, scope, options) super @uniq = !!options[:uniq] @flatten = options[:flatten] @priority_on_named = options[:priority_on_named].nil? ? true : options[:priority_on_name] @priority_on_unnamed = !!options[:priority_on_unnamed] case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end end protected # @api private def internal_produce(scope) seen = {} included_keys = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name existing = seen[name] empty_name = name.nil? || name.empty? if existing if empty_name && priority_on_unnamed if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}' with unnamed entry." end next elsif !empty_name && priority_on_named if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}', key: '#{name}'." end next end else seen[name] = entry end included_keys << key end result = included_keys.collect do |k| x = injector.lookup_key(scope, k) assert_type(binding(), injector.type_calculator(), x) x end result.flatten!(flatten) if flatten result.uniq! if uniq result end # @api private def assert_type(binding, tc, value) infered = tc.infer(value) unless tc.assignable?(binding.type.element_type, infered) || tc.assignable?(binding.type, infered) raise ArgumentError, ["Type Error: contribution to '#{binding.name}' does not match type of multibind, ", "#{type_error_detail([binding.type.element_type, binding.type], value)}"].join() end end end # @api public class HashMultibindProducer < MultibindProducer # @return [Symbol] One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` # @api public attr_reader :conflict_resolution # @return [Boolean] # @api public attr_reader :uniq # @return [Boolean, Integer] Flatten all if true, or none if false, or to given level (0 = none, -1 = all) # @api public attr_reader :flatten # The hash multibind producer provides options to control conflict resolution. # By default, the hash is produced using `:priority` resolution - the highest entry is selected, the rest are # ignored unless they have the same priority which is an error. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Symbol, String] :conflict_resolution (:priority) One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` #
  • `ignore` the first found highest priority contribution is used, the rest are ignored
  • #
  • `error` any duplicate key is an error
  • #
  • `append` element type must be compatible with Array, makes elements be arrays and appends all found
  • #
  • `merge` element type must be compatible with hash, merges hashes with retention of highest priority hash content
  • #
  • `priority` the first found highest priority contribution is used, duplicates with same priority raises and error, the rest are # ignored.
# @option options [Boolean, Integer] :flatten (false) If appended should be flattened. Also see {#flatten}. # @option options [Boolean] :uniq (false) If appended result should be made unique. # # @api public # def initialize(injector, binding, scope, options) super @conflict_resolution = options[:conflict_resolution].nil? ? :priority : options[:conflict_resolution] @uniq = !!options[:uniq] @flatten = options[:flatten] unless [:error, :merge, :append, :priority, :ignore].include?(@conflict_resolution) raise ArgumentError, "Unknown conflict_resolution for Multibind Hash: '#{@conflict_resolution}." end case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end if uniq || flatten || conflict_resolution.to_s == 'append' etype = binding.type.element_type unless etype.class == Puppet::Pops::Types::PDataType || etype.is_a?(Puppet::Pops::Types::PArrayType) detail = [] detail << ":uniq" if uniq detail << ":flatten" if flatten detail << ":conflict_resolution => :append" if conflict_resolution.to_s == 'append' raise ArgumentError, ["Options #{detail.join(', and ')} cannot be used with a Multibind ", "of type #{injector.type_calculator.string(binding.type)}"].join() end end end protected # @api private def internal_produce(scope) seen = {} included_entries = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name raise ArgumentError, "A Hash Multibind contribution to '#{binding.name}' must have a name." if name.nil? || name.empty? existing = seen[name] if existing case conflict_resolution.to_s when 'priority' # skip if duplicate has lower prio if (comparison = (seen[name] <=> entry)) <= 0 raise ArgumentError, "Internal Error: contributions not given in decreasing precedence order" unless comparison == 0 raise ArgumentError, "Duplicate key (same priority) contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end next when 'ignore' # skip, ignore conflict if prio is the same next when 'error' raise ArgumentError, "Duplicate key contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end else seen[name] = entry end included_entries << [key, entry] end result = {} included_entries.each do |element| k = element[ 0 ] entry = element[ 1 ] x = injector.lookup_key(scope, k) name = entry.binding.name assert_type(binding(), injector.type_calculator(), name, x) if result[ name ] merge(result, name, result[ name ], x) else result[ name ] = conflict_resolution().to_s == 'append' ? [x] : x end end result end # @api private def merge(result, name, higher, lower) case conflict_resolution.to_s when 'append' unless higher.is_a?(Array) higher = [higher] end tmp = higher + [lower] tmp.flatten!(flatten) if flatten tmp.uniq! if uniq result[name] = tmp when 'merge' result[name] = lower.merge(higher) end end # @api private def assert_type(binding, tc, key, value) unless tc.instance?(binding.type.key_type, key) raise ArgumentError, ["Type Error: key contribution to #{binding.name}['#{key}'] ", "is incompatible with key type: #{tc.label(binding.type)}, ", type_error_detail(binding.type.key_type, key)].join() end if key.nil? || !key.is_a?(String) || key.empty? raise ArgumentError, "Entry contributing to multibind hash with id '#{binding.id}' must have a name." end unless tc.instance?(binding.type.element_type, value) raise ArgumentError, ["Type Error: value contribution to #{binding.name}['#{key}'] ", "is incompatible, ", type_error_detail(binding.type.element_type, value)].join() end end end end diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index 0aca525c1..644bbd0e3 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,228 +1,228 @@ # A Closure represents logic bound to a particular scope. # As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not # safe to use this closure when the scope given to it when initialized goes "out of scope". # # Note that the implementation is backwards compatible in that the call method accepts a scope, but this # scope is not used. # # Note that this class is a CallableSignature, and the methods defined there should be used # as the API for obtaining information in a callable implementation agnostic way. # class Puppet::Pops::Evaluator::Closure < Puppet::Pops::Evaluator::CallableSignature attr_reader :evaluator attr_reader :model attr_reader :enclosing_scope def initialize(evaluator, model, scope) @evaluator = evaluator @model = model @enclosing_scope = scope end # marker method checked with respond_to :puppet_lambda # @api private # @deprecated Use the type system to query if an object is of Callable type, then use its signatures method for info def puppet_lambda() true end # compatible with 3x AST::Lambda # @api public - def call(scope, *args) + def call(*args) variable_bindings = combine_values_with_parameters(args) tc = Puppet::Pops::Types::TypeCalculator final_args = tc.infer_set(parameters.inject([]) do |final_args, param| if param.captures_rest final_args.concat(variable_bindings[param.name]) else final_args << variable_bindings[param.name] end end) if tc.callable?(type, final_args) @evaluator.evaluate_block_with_bindings(@enclosing_scope, variable_bindings, @model.body) else raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" end end # Call closure with argument assignment by name def call_by_name(scope, args_hash, enforce_parameters) if enforce_parameters if args_hash.size > parameters.size raise ArgumentError, "Too many arguments: #{args_hash.size} for #{parameters.size}" end # associate values with parameters scope_hash = {} parameters.each do |p| name = p.name if (arg_value = args_hash[name]).nil? # only set result of default expr if it is defined (it is otherwise not possible to differentiate # between explicit undef and no default expression unless p.value.nil? scope_hash[name] = @evaluator.evaluate(p.value, @enclosing_scope) end else scope_hash[name] = arg_value end end missing = parameters.select { |p| !scope_hash.include?(p.name) } if missing.any? raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.collect(&:name).join(" ,")}" end tc = Puppet::Pops::Types::TypeCalculator final_args = tc.infer_set(parameter_names.collect { |param| scope_hash[param] }) if !tc.callable?(type, final_args) raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" end else scope_hash = args_hash end @evaluator.evaluate_block_with_bindings(@enclosing_scope, scope_hash, @model.body) end def parameters @model.parameters end # Returns the number of parameters (required and optional) # @return [Integer] the total number of accepted parameters def parameter_count # yes, this is duplication of code, but it saves a method call @model.parameters.size end # @api public def parameter_names @model.parameters.collect(&:name) end # @api public def type @callable ||= create_callable_type end # @api public def last_captures_rest? last = @model.parameters[-1] last && last.captures_rest end # @api public def block_name # TODO: Lambda's does not support blocks yet. This is a placeholder 'unsupported_block' end private def combine_values_with_parameters(args) variable_bindings = {} parameters.each_with_index do |parameter, index| param_captures = parameter.captures_rest default_expression = parameter.value if index >= args.size if default_expression # not given, has default value = @evaluator.evaluate(default_expression, @enclosing_scope) if param_captures && !value.is_a?(Array) # correct non array default value value = [value] end else # not given, does not have default if param_captures # default for captures rest is an empty array value = [] else @evaluator.fail(Puppet::Pops::Issues::MISSING_REQUIRED_PARAMETER, parameter, { :param_name => parameter.name }) end end else given_argument = args[index] if param_captures # get excess arguments value = args[(parameter_count-1)..-1] # If the input was a single nil, or undef, and there is a default, use the default # This supports :undef in case it was used in a 3x data structure and it is passed as an arg # if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression value = @evaluator.evaluate(default_expression, scope) # and ensure it is an array value = [value] unless value.is_a?(Array) end else value = given_argument end end variable_bindings[parameter.name] = value end variable_bindings end def create_callable_type() types = [] range = [0, 0] in_optional_parameters = false parameters.each do |param| type = if param.type_expr @evaluator.evaluate(param.type_expr, @enclosing_scope) else Puppet::Pops::Types::TypeFactory.any() end if param.captures_rest && type.is_a?(Puppet::Pops::Types::PArrayType) # An array on a slurp parameter is how a size range is defined for a # slurp (Array[Integer, 1, 3] *$param). However, the callable that is # created can't have the array in that position or else type checking # will require the parameters to be arrays, which isn't what is # intended. The array type contains the intended information and needs # to be unpacked. param_range = type.size_range type = type.element_type elsif param.captures_rest && !type.is_a?(Puppet::Pops::Types::PArrayType) param_range = ANY_NUMBER_RANGE elsif param.value param_range = OPTIONAL_SINGLE_RANGE else param_range = REQUIRED_SINGLE_RANGE end types << type if param_range[0] == 0 in_optional_parameters = true elsif param_range[0] != 0 && in_optional_parameters @evaluator.fail(Puppet::Pops::Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name }) end range[0] += param_range[0] range[1] += param_range[1] end if range[1] == Puppet::Pops::Types::INFINITY range[1] = :default end Puppet::Pops::Types::TypeFactory.callable(*(types + range)) end # Produces information about parameters compatible with a 4x Function (which can have multiple signatures) def signatures [ self ] end ANY_NUMBER_RANGE = [0, Puppet::Pops::Types::INFINITY] OPTIONAL_SINGLE_RANGE = [0, 1] REQUIRED_SINGLE_RANGE = [1, 1] end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index cc319cd28..ef428b2c6 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,692 +1,692 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' module FunctionAPISpecModule class TestDuck end class TestFunctionLoader < Puppet::Pops::Loader::StaticLoader def initialize @functions = {} end def add_function(name, function) typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, name) entry = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, function, __FILE__) @functions[typed_name] = entry end # override StaticLoader def load_constant(typed_name) @functions[typed_name] end end end describe 'the 4x function api' do include FunctionAPISpecModule include PuppetSpec::Pops include PuppetSpec::Scope let(:loader) { FunctionAPISpecModule::TestFunctionLoader.new } it 'allows a simple function to be created without dispatch declaration' do f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end # the produced result is a Class inheriting from Function expect(f.class).to be(Class) expect(f.superclass).to be(Puppet::Functions::Function) # and this class had the given name (not a real Ruby class name) expect(f.name).to eql('min') end it 'refuses to create functions that are not based on the Function class' do expect do Puppet::Functions.create_function('testing', Object) {} end.to raise_error(ArgumentError, 'Functions must be based on Puppet::Pops::Functions::Function. Got Object') end it 'a function without arguments can be defined and called without dispatch declaration' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(10) end it 'an error is raised when calling a no arguments function with arguments' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect{func.call({}, 'surprise')}.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test() - arg count {0} actual: test(String) - arg count {1}") end it 'a simple function can be called' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect(func.call({}, 10,20)).to eql(10) end it 'an error is raised if called with too few arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer) - arg count {1}") end it 'an error is raised if called with too many arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error is raised if simple function-name and method are not matched' do expect do f = create_badly_named_method_function_class() end.to raise_error(ArgumentError, /Function Creation Error, cannot create a default dispatcher for function 'mix', no method with this name found/) end it 'the implementation separates dispatchers for different functions' do # this tests that meta programming / construction puts class attributes in the correct class f1 = create_min_function_class() f2 = create_max_function_class() d1 = f1.dispatcher d2 = f2.dispatcher expect(d1).to_not eql(d2) expect(d1.dispatchers[0]).to_not eql(d2.dispatchers[0]) end context 'when using regular dispatch' do it 'a function can be created using dispatch and called' do f = create_min_function_class_using_dispatch() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) end it 'an error is raised with reference to given parameter names when called with mis-matched arguments' do f = create_min_function_class_using_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(Numeric a, Numeric b) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error includes optional indicators and count for last element' do f = create_function_with_optionals_and_varargs() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2,}' else 'Any x, Any y, Any a?, Any b?, Any c{0,}' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'an error includes optional indicators and count for last element when defined via dispatch' do f = create_function_with_optionals_and_varargs_via_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(Numeric x, Numeric y, Numeric a?, Numeric b?, Numeric c{0,}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'a function can be created using dispatch and called' do f = create_min_function_class_disptaching_to_two_methods() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) expect(func.call({}, 'Apple', 'Banana')).to eql('Apple') end it 'an error is raised with reference to multiple methods when called with mis-matched arguments' do f = create_min_function_class_disptaching_to_two_methods() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected one of: min(Numeric a, Numeric b) - arg count {2} min(String s1, String s2) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}") end context 'can use injection' do before :all do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('evoe') bind.name('an_int').to(42) end Puppet.push_context({:injector => injector}, "injector for testing function API") end after :all do Puppet.pop_context() end it 'attributes can be injected' do f1 = create_function_with_class_injection() f = f1.new(:closure_scope, :loader) expect(f.test_attr2()).to eql("evoe") expect(f.serial().produce(nil)).to eql(42) expect(f.test_attr().class.name).to eql("FunctionAPISpecModule::TestDuck") end it 'parameters can be injected and woven with regular dispatch' do f1 = create_function_with_param_injection_regular() f = f1.new(:closure_scope, :loader) expect(f.call(nil, 10, 20)).to eql("evoe! 10, and 20 < 42 = true") expect(f.call(nil, 50, 20)).to eql("evoe! 50, and 20 < 42 = false") end end context 'when requesting a type' do it 'responds with a Callable for a single signature' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_using_dispatch() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PCallableType) expect(t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t.block_type).to be_nil end it 'responds with a Variant[Callable...] for multiple signatures' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_disptaching_to_two_methods() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PVariantType) expect(t.types.size).to eql(2) t1 = t.types[0] expect(t1.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t1.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t1.block_type).to be_nil t2 = t.types[1] expect(t2.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t2.param_types.types).to eql([tf.string(), tf.string()]) expect(t2.block_type).to be_nil end end context 'supports lambdas' do it 'such that, a required block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, a missing required block when called raises an error' do # use a Function as callable the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) expect do the_function.call({}, 10) end.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test(Integer x, Callable block) - arg count {2} actual: test(Integer) - arg count {1}") end it 'such that, an optional block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, an optional block can be omitted when called and gets the value nil' do # use a Function as callable the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 10)).to be_nil end it 'such that, a scope can be injected and a block can be used' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_scope_required_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 10, the_callable)).to be(the_callable) end end context 'provides signature information' do it 'about capture rest (varargs)' do fc = create_function_with_optionals_and_varargs signatures = fc.signatures expect(signatures.size).to eql(1) signature = signatures[0] expect(signature.last_captures_rest?).to be_true end it 'about optional and required parameters' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.args_range).to eql( [2, Puppet::Pops::Types::INFINITY ] ) expect(signature.infinity?(signature.args_range[1])).to be_true end it 'about block not being allowed' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 0 ] ) expect(signature.block_type).to be_nil end it 'about required block' do fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 1, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about optional block' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about the type' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.type.class).to be(Puppet::Pops::Types::PCallableType) end # conditional on Ruby 1.8.7 which does not do parameter introspection if Method.method_defined?(:parameters) it 'about parameter names obtained from ruby introspection' do fc = create_min_function_class signature = fc.signatures[0] expect(signature.parameter_names).to eql(['x', 'y']) end end it 'about parameter names specified with dispatch' do fc = create_min_function_class_using_dispatch signature = fc.signatures[0] expect(signature.parameter_names).to eql(['a', 'b']) end it 'about block_name when it is *not* given in the definition' do # neither type, nor name fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_name).to eql('block') # no name given, only type fc = create_function_with_required_block_given_type signature = fc.signatures[0] expect(signature.block_name).to eql('block') end it 'about block_name when it *is* given in the definition' do # neither type, nor name fc = create_function_with_required_block_default_type signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') # no name given, only type fc = create_function_with_required_block_fully_specified signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') end end context 'supports calling other functions' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end it 'such that, other functions are callable by name' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function available in the puppet system call_function('assert_type', 'Integer', 10) end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect(f.call({})).to eql(10) end it 'such that, calling a non existing function raises an error' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function not available in the puppet system call_function('no_such_function', 'Integer', 'hello') end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect{f.call({})}.to raise_error(ArgumentError, "Function test(): cannot call function 'no_such_function' - not found") end end context 'supports calling ruby functions with lambda from puppet' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end 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 # Puppet[:parser] = 'future' # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) require 'puppetx' end let(:parser) { Puppet::Pops::Parser::EvaluatingParser.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it 'function with required block can be called' do # construct ruby function to call fc = Puppet::Functions.create_function('testing::test') do dispatch :test do param 'Integer', 'x' # block called 'the_block', and using "all_callables" required_block_param #(all_callables(), 'the_block') end def test(x, block) # call the block with x - block.call(closure_scope, x) + block.call(x) end end # add the function to the loader (as if it had been loaded from somewhere) the_loader = loader() f = fc.new({}, the_loader) loader.add_function('testing::test', f) # evaluate a puppet call source = "testing::test(10) |$x| { $x+1 }" program = parser.parse_string(source, __FILE__) Puppet::Pops::Adapters::LoaderAdapter.adapt(program.model).loader = the_loader expect(parser.evaluate(scope, program)).to eql(11) end end end def create_noargs_function_class f = Puppet::Functions.create_function('test') do def test() 10 end end end def create_min_function_class f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end end def create_max_function_class f = Puppet::Functions.create_function('max') do def max(x,y) x >= y ? x : y end end end def create_badly_named_method_function_class f = Puppet::Functions.create_function('mix') do def mix_up(x,y) x <= y ? x : y end end end def create_min_function_class_using_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end def min(x,y) x <= y ? x : y end end end def create_min_function_class_disptaching_to_two_methods f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end dispatch :min_s do param 'String', 's1' param 'String', 's2' end def min(x,y) x <= y ? x : y end def min_s(x,y) cmp = (x.downcase <=> y.downcase) cmp <= 0 ? x : y end end end def create_function_with_optionals_and_varargs f = Puppet::Functions.create_function('min') do def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_optionals_and_varargs_via_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'x' param 'Numeric', 'y' param 'Numeric', 'a' param 'Numeric', 'b' param 'Numeric', 'c' arg_count 2, :default end def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_class_injection f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" def test(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_param_injection_regular f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" dispatch :test do injected_param Puppet::Pops::Types::TypeFactory.string, 'x', 'a_string' injected_producer_param Puppet::Pops::Types::TypeFactory.integer, 'y', 'an_int' param 'Scalar', 'a' param 'Scalar', 'b' end def test(x,y,a,b) y_produced = y.produce(nil) "#{x}! #{a}, and #{b} < #{y_produced} = #{ !!(a < y_produced && b < y_produced)}" end end end def create_function_with_required_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_scope_required_block_all_defaults f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do dispatch :test do scope_param param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param end def test(scope, x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_default_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param 'the_block' end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_given_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_fully_specified f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param('Callable', 'the_block') end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_optional_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' optional_block_param end def test(x, block=nil) # returns the block to make it easy to test what it got when called # a default of nil must be used or the call will fail with a missing parameter block end end end end diff --git a/spec/unit/pops/binder/injector_spec.rb b/spec/unit/pops/binder/injector_spec.rb index b62ceaeb9..7feeb4192 100644 --- a/spec/unit/pops/binder/injector_spec.rb +++ b/spec/unit/pops/binder/injector_spec.rb @@ -1,786 +1,786 @@ require 'spec_helper' require 'puppet/pops' module InjectorSpecModule def injector(binder) Puppet::Pops::Binder::Injector.new(binder) end def factory Puppet::Pops::Binder::BindingsFactory end def test_layer_with_empty_bindings factory.named_layer('test-layer', factory.named_bindings('test').model) end def test_layer_with_bindings(*bindings) factory.named_layer('test-layer', *bindings) end def null_scope() nil end def type_calculator Puppet::Pops::Types::TypeCalculator end def type_factory Puppet::Pops::Types::TypeFactory end # Returns a binder # def configured_binder b = Puppet::Pops::Binder::Binder.new() b end class TestDuck end class Daffy < TestDuck end class AngryDuck < TestDuck # Supports assisted inject, returning a Donald duck as the default impl of Duck def self.inject(injector, scope, binding, *args) Donald.new() end end class Donald < AngryDuck end class ArneAnka < AngryDuck attr_reader :label def initialize() @label = 'A Swedish angry cartoon duck' end end class ScroogeMcDuck < TestDuck attr_reader :fortune # Supports assisted inject, returning an ScroogeMcDuck with 1$ fortune or first arg in args # Note that when injected (via instance producer, or implict assisted inject, the inject method # always wins. def self.inject(injector, scope, binding, *args) self.new(args[0].nil? ? 1 : args[0]) end def initialize(fortune) @fortune = fortune end end class NamedDuck < TestDuck attr_reader :name def initialize(name) @name = name end end # Test custom producer that on each produce returns a duck that is twice as rich as its predecessor class ScroogeProducer < Puppet::Pops::Binder::Producers::Producer attr_reader :next_capital def initialize @next_capital = 100 end def produce(scope) ScroogeMcDuck.new(@next_capital *= 2) end end end describe 'Injector' do include InjectorSpecModule let(:bindings) { factory.named_bindings('test') } let(:scope) { null_scope()} let(:binder) { Puppet::Pops::Binder::Binder } let(:lbinder) do binder.new(layered_bindings) end def duck_type # create distinct instances type_factory.ruby(InjectorSpecModule::TestDuck) end let(:layered_bindings) { factory.layered_bindings(test_layer_with_bindings(bindings.model)) } context 'When created' do it 'should not raise an error if binder is configured' do expect { injector(lbinder) }.to_not raise_error end it 'should create an empty injector given an empty binder' do expect { binder.new(layered_bindings) }.to_not raise_exception end it "should be possible to reference the TypeCalculator" do injector(lbinder).type_calculator.is_a?(Puppet::Pops::Types::TypeCalculator).should == true end it "should be possible to reference the KeyFactory" do injector(lbinder).key_factory.is_a?(Puppet::Pops::Binder::KeyFactory).should == true end it "can be created using a model" do bindings.bind.name('a_string').to('42') injector = Puppet::Pops::Binder::Injector.create_from_model(layered_bindings) injector.lookup(scope, 'a_string').should == '42' end it 'can be created using a block' do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('42') end injector.lookup(scope, 'a_string').should == '42' end it 'can be created using a hash' do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') injector.lookup(scope, 'a_string').should == '42' end it 'can be created using an overriding injector with block' do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('42') end injector2 = injector.override('override') do bind.name('a_string').to('43') end injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end it 'can be created using an overriding injector with hash' do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') injector2 = injector.override_with_hash('override', 'a_string' => '43') injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end it "can be created using an overriding injector with a model" do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') bindings.bind.name('a_string').to('43') injector2 = injector.override_with_model(layered_bindings) injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end end context "When looking up objects" do it 'lookup(scope, name) finds bound object of type Data with given name' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string').should == '42' end context 'a block transforming the result can be given' do it 'that transform a found value given scope and value' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string') {|zcope, val| val + '42' }.should == '4242' end it 'that transform a found value given only value' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string') {|val| val + '42' }.should == '4242' end it 'that produces a default value when entry is missing' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_non_existing_string') {|val| val ? (raise Error, "Should not happen") : '4242' }.should == '4242' end end context "and class is not bound" do it "assisted inject kicks in for classes with zero args constructor" do duck_type = type_factory.ruby(InjectorSpecModule::Daffy) injector = injector(lbinder) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Daffy).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Daffy).should == true end it "assisted inject produces same instance on lookup but not on lookup producer" do duck_type = type_factory.ruby(InjectorSpecModule::Daffy) injector = injector(lbinder) d1 = injector.lookup(scope, duck_type) d2 = injector.lookup(scope, duck_type) d1.equal?(d2).should == true d1 = injector.lookup_producer(scope, duck_type).produce(scope) d2 = injector.lookup_producer(scope, duck_type).produce(scope) d1.equal?(d2).should == false end it "assisted inject kicks in for classes with a class inject method" do duck_type = type_factory.ruby(InjectorSpecModule::ScroogeMcDuck) injector = injector(lbinder) # Do not pass any arguments, the ScroogeMcDuck :inject method should pick 1 by default # This tests zero args passed injector.lookup(scope, duck_type).fortune.should == 1 injector.lookup_producer(scope, duck_type).produce(scope).fortune.should == 1 end it "assisted inject selects the inject method if it exists over a zero args constructor" do injector = injector(lbinder) duck_type = type_factory.ruby(InjectorSpecModule::AngryDuck) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Donald).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Donald).should == true end it "assisted inject selects the zero args constructor if injector is from a superclass" do injector = injector(lbinder) duck_type = type_factory.ruby(InjectorSpecModule::ArneAnka) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::ArneAnka).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::ArneAnka).should == true end end context "and multiple layers are in use" do it "a higher layer shadows anything in a lower layer" do bindings1 = factory.named_bindings('test1') bindings1.bind().name('a_string').to('bad stuff') lower_layer = factory.named_layer('lower-layer', bindings1.model) bindings2 = factory.named_bindings('test2') bindings2.bind().name('a_string').to('good stuff') higher_layer = factory.named_layer('higher-layer', bindings2.model) injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer))) injector.lookup(scope,'a_string').should == 'good stuff' end it "a higher layer may not shadow a lower layer binding that is final" do bindings1 = factory.named_bindings('test1') bindings1.bind().final.name('a_string').to('required stuff') lower_layer = factory.named_layer('lower-layer', bindings1.model) bindings2 = factory.named_bindings('test2') bindings2.bind().name('a_string').to('contraband') higher_layer = factory.named_layer('higher-layer', bindings2.model) expect { injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer))) }.to raise_error(/Override of final binding not allowed/) end end context "and dealing with Data types" do let(:lbinder) { binder.new(layered_bindings) } it "should treat all data as same type w.r.t. key" do bindings.bind().name('a_string').to('42') bindings.bind().name('an_int').to(43) bindings.bind().name('a_float').to(3.14) bindings.bind().name('a_boolean').to(true) bindings.bind().name('an_array').to([1,2,3]) bindings.bind().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3}) injector = injector(lbinder) injector.lookup(scope,'a_string').should == '42' injector.lookup(scope,'an_int').should == 43 injector.lookup(scope,'a_float').should == 3.14 injector.lookup(scope,'a_boolean').should == true injector.lookup(scope,'an_array').should == [1,2,3] injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} end it "should provide type-safe lookup of given type/name" do bindings.bind().string().name('a_string').to('42') bindings.bind().integer().name('an_int').to(43) bindings.bind().float().name('a_float').to(3.14) bindings.bind().boolean().name('a_boolean').to(true) bindings.bind().array_of_data().name('an_array').to([1,2,3]) bindings.bind().hash_of_data().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3}) injector = injector(lbinder) # Check lookup using implied Data type injector.lookup(scope,'a_string').should == '42' injector.lookup(scope,'an_int').should == 43 injector.lookup(scope,'a_float').should == 3.14 injector.lookup(scope,'a_boolean').should == true injector.lookup(scope,'an_array').should == [1,2,3] injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} # Check lookup using expected type injector.lookup(scope,type_factory.string(), 'a_string').should == '42' injector.lookup(scope,type_factory.integer(), 'an_int').should == 43 injector.lookup(scope,type_factory.float(),'a_float').should == 3.14 injector.lookup(scope,type_factory.boolean(),'a_boolean').should == true injector.lookup(scope,type_factory.array_of_data(),'an_array').should == [1,2,3] injector.lookup(scope,type_factory.hash_of_data(),'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} # Check lookup using wrong type expect { injector.lookup(scope,type_factory.integer(), 'a_string')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(), 'an_int')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_float')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_boolean')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'an_array')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_hash')}.to raise_error(/Type error/) end end end context "When looking up producer" do it 'the value is produced by calling produce(scope)' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string').produce(scope).should == '42' end context 'a block transforming the result can be given' do it 'that transform a found value given scope and producer' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string') {|zcope, p| p.produce(zcope) + '42' }.should == '4242' end it 'that transform a found value given only producer' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string') {|p| p.produce(scope) + '42' }.should == '4242' end it 'that can produce a default value when entry is not found' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_non_existing_string') {|p| p ? (raise Error,"Should not happen") : '4242' }.should == '4242' end end end context "When dealing with singleton vs. non singleton" do it "should produce the same instance when producer is a singleton" do bindings.bind().name('a_string').to('42') injector = injector(lbinder) a = injector.lookup(scope, 'a_string') b = injector.lookup(scope, 'a_string') a.equal?(b).should == true end it "should produce different instances when producer is a non singleton producer" do bindings.bind().name('a_string').to_series_of('42') injector = injector(lbinder) a = injector.lookup(scope, 'a_string') b = injector.lookup(scope, 'a_string') a.should == '42' b.should == '42' a.equal?(b).should == false end end context "When using the lookup producer" do it "should lookup again to produce a value" do bindings.bind().name('a_string').to_lookup_of('another_string') bindings.bind().name('another_string').to('hello') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end it "should produce nil if looked up key does not exist" do bindings.bind().name('a_string').to_lookup_of('non_existing') injector(lbinder).lookup(scope, 'a_string').should == nil end it "should report an error if lookup loop is detected" do bindings.bind().name('a_string').to_lookup_of('a_string') expect { injector(lbinder).lookup(scope, 'a_string') }.to raise_error(/Lookup loop/) end end context "When using the hash lookup producer" do it "should lookup a key in looked up hash" do data_hash = type_factory.hash_of_data() bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'a_hash', 'huey') bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'}) injector(lbinder).lookup(scope, 'a_string').should == 'red' end it "should produce nil if looked up entry does not exist" do data_hash = type_factory.hash_of_data() bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'non_existing_entry', 'huey') bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'}) injector(lbinder).lookup(scope, 'a_string').should == nil end end context "When using the first found producer" do it "should lookup until it finds a value, but not further" do bindings.bind().name('a_string').to_first_found('b_string', 'c_string', 'g_string') bindings.bind().name('c_string').to('hello') bindings.bind().name('g_string').to('Oh, mrs. Smith...') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end it "should lookup until it finds a value using mix of type and name, but not further" do bindings.bind().name('a_string').to_first_found('b_string', [type_factory.string, 'c_string'], 'g_string') bindings.bind().name('c_string').to('hello') bindings.bind().name('g_string').to('Oh, mrs. Smith...') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end end context "When producing instances" do it "should lookup an instance of a class without arguments" do bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::Daffy) injector(lbinder).lookup(scope, duck_type, 'the_duck').is_a?(InjectorSpecModule::Daffy).should == true end it "should lookup an instance of a class with arguments" do bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::ScroogeMcDuck, 1234) injector = injector(lbinder) the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 1234 end it "singleton producer should not be recreated between lookups" do bindings.bind().type(duck_type).name('the_duck').to_producer(InjectorSpecModule::ScroogeProducer) injector = injector(lbinder) the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 200 # singleton, do it again to get next value in series - it is the producer that is a singleton # not the produced value the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 400 duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune.should == 800 end it "series of producers should recreate producer on each lookup and lookup_producer" do bindings.bind().type(duck_type).name('the_duck').to_producer_series(InjectorSpecModule::ScroogeProducer) injector = injector(lbinder) duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune().should == 200 duck_producer.produce(scope).fortune().should == 400 # series, each lookup gets a new producer (initialized to produce 200) duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune().should == 200 duck_producer.produce(scope).fortune().should == 400 injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200 injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200 end end context "When working with multibind" do context "of hash kind" do it "a multibind produces contributed items keyed by their bound key-name" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew2').to(InjectorSpecModule::NamedDuck, 'Dewey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew3').to(InjectorSpecModule::NamedDuck, 'Louie') injector = injector(lbinder) the_ducks = injector.lookup(scope, hash_of_duck, "donalds_nephews") the_ducks.size.should == 3 the_ducks['nephew1'].name.should == 'Huey' the_ducks['nephew2'].name.should == 'Dewey' the_ducks['nephew3'].name.should == 'Louie' end it "is an error to not bind contribution with a name" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') # missing name bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to raise_error(/must have a name/) end it "is an error to bind with duplicate key when using default (priority) conflict resolution" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') # missing name bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to raise_error(/Duplicate key/) end it "is not an error to bind with duplicate key when using (ignore) conflict resolution" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews').producer_options(:conflict_resolution => :ignore) bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to_not raise_error end it "should produce detailed type error message" do hash_of_integer = type_factory.hash_of(type_factory.integer()) multibind_id = "ints" mb = bindings.multibind(multibind_id).type(hash_of_integer).name('donalds_family') bindings.bind.in_multibind(multibind_id).name('nephew').to('Huey') expect { ducks = injector(lbinder).lookup(scope, 'donalds_family') }.to raise_error(%r{expected: Integer, got: String}) end it "should be possible to combine hash multibind contributions with append on conflict" do # This case uses a multibind of individual strings, but combines them # into an array bound to a hash key # (There are other ways to do this - e.g. have the multibind lookup a multibind # of array type to which nephews are contributed). # hash_of_data = type_factory.hash_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family') mb.producer_options(:conflict_resolution => :append) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck') bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake') ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks['nephews'].should == ['Huey', 'Dewey', 'Louie'] ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake'] end it "should be possible to combine hash multibind contributions with append, flat, and uniq, on conflict" do # This case uses a multibind of individual strings, but combines them # into an array bound to a hash key # (There are other ways to do this - e.g. have the multibind lookup a multibind # of array type to which nephews are contributed). # hash_of_data = type_factory.hash_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family') mb.producer_options(:conflict_resolution => :append, :flatten => true, :uniq => true) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to(['Huey', ['Louie'], 'Dewey']) bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck') bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake') ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks['nephews'].should == ['Huey', 'Dewey', 'Louie'] ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake'] end it "should fail attempts to append, perform uniq or flatten on type incompatible multibind hash" do hash_of_integer = type_factory.hash_of(type_factory.integer()) ids = ["ducks1", "ducks2", "ducks3"] mb = bindings.multibind(ids[0]).type(hash_of_integer.copy).name('broken_family0') mb.producer_options(:conflict_resolution => :append) mb = bindings.multibind(ids[1]).type(hash_of_integer.copy).name('broken_family1') mb.producer_options(:flatten => :true) mb = bindings.multibind(ids[2]).type(hash_of_integer.copy).name('broken_family2') mb.producer_options(:uniq => :true) injector = injector(binder.new(factory.layered_bindings(test_layer_with_bindings(bindings.model)))) expect { injector.lookup(scope, 'broken_family0')}.to raise_error(/:conflict_resolution => :append/) expect { injector.lookup(scope, 'broken_family1')}.to raise_error(/:flatten/) expect { injector.lookup(scope, 'broken_family2')}.to raise_error(/:uniq/) end it "a higher priority contribution is selected when resolution is :priority" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') mb1 = bindings.bind.in_multibind(multibind_id) pending 'priority based on layers not added, and priority on category removed' mb1.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Huey') mb2 = bindings.bind.in_multibind(multibind_id) mb2.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Dewey') binder.define_layers(layered_bindings) injector(binder).lookup(scope, hash_of_duck, "donalds_nephews")['nephew'].name.should == 'Huey' end it "a higher priority contribution wins when resolution is :merge" do # THIS TEST MAY DEPEND ON HASH ORDER SINCE PRIORITY BASED ON CATEGORY IS REMOVED hash_of_data = type_factory.hash_of_data() multibind_id = "hashed_ducks" bindings.multibind(multibind_id).type(hash_of_data).name('donalds_nephews').producer_options(:conflict_resolution => :merge) mb1 = bindings.bind.in_multibind(multibind_id) mb1.name('nephew').to({'name' => 'Huey', 'is' => 'winner'}) mb2 = bindings.bind.in_multibind(multibind_id) mb2.name('nephew').to({'name' => 'Dewey', 'is' => 'looser', 'has' => 'cap'}) the_ducks = injector(binder.new(layered_bindings)).lookup(scope, "donalds_nephews"); the_ducks['nephew']['name'].should == 'Huey' the_ducks['nephew']['is'].should == 'winner' the_ducks['nephew']['has'].should == 'cap' end end context "of array kind" do it "an array multibind produces contributed items, names are allowed but ignored" do array_of_duck = type_factory.array_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(array_of_duck).name('donalds_nephews') # one with name (ignored, expect no error) bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey') # two without name bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey') bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Louie') the_ducks = injector(lbinder).lookup(scope, array_of_duck, "donalds_nephews") the_ducks.size.should == 3 the_ducks.collect {|d| d.name }.sort.should == ['Dewey', 'Huey', 'Louie'] end it "should be able to make result contain only unique entries" do # This case uses a multibind of individual strings, and combines them # into an array of unique values # array_of_data = type_factory.array_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(array_of_data).name('donalds_family') # turn off priority on named to not trigger conflict as all additions have the same precedence # (could have used the default for unnamed and add unnamed entries). mb.producer_options(:priority_on_named => false, :uniq => true) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') # duplicate bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks.should == ['Huey', 'Dewey', 'Louie'] end it "should be able to contribute elements and arrays of elements and flatten 1 level" do # This case uses a multibind of individual strings and arrays, and combines them # into an array of flattened # array_of_string = type_factory.array_of(type_factory.string()) multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(array_of_string).name('donalds_family') # flatten one level mb.producer_options(:flatten => 1) bindings.bind.in_multibind(multibind_id).to('Huey') bindings.bind.in_multibind(multibind_id).to('Dewey') bindings.bind.in_multibind(multibind_id).to('Louie') # duplicate bindings.bind.in_multibind(multibind_id).to(['Huey', 'Dewey', 'Louie']) ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks.should == ['Huey', 'Dewey', 'Louie', 'Huey', 'Dewey', 'Louie'] end it "should produce detailed type error message" do array_of_integer = type_factory.array_of(type_factory.integer()) multibind_id = "ints" mb = bindings.multibind(multibind_id).type(array_of_integer).name('donalds_family') bindings.bind.in_multibind(multibind_id).to('Huey') expect { ducks = injector(lbinder).lookup(scope, 'donalds_family') }.to raise_error(%r{expected: Integer, or Array\[Integer\], got: String}) end end context "When using multibind in multibind" do it "a hash multibind can be contributed to another" do hash_of_data = type_factory.hash_of_data() mb1_id = 'data1' mb2_id = 'data2' top = bindings.multibind(mb1_id).type(hash_of_data).name("top") detail = bindings.multibind(mb2_id).type(hash_of_data).name("detail").in_multibind(mb1_id) bindings.bind.in_multibind(mb1_id).name('a').to(10) bindings.bind.in_multibind(mb1_id).name('b').to(20) bindings.bind.in_multibind(mb2_id).name('a').to(30) bindings.bind.in_multibind(mb2_id).name('b').to(40) expect( injector(lbinder).lookup(scope, "top") ).to eql({'detail' => {'a' => 30, 'b' => 40}, 'a' => 10, 'b' => 20}) end end context "When looking up entries requiring evaluation" do let(:node) { Puppet::Node.new('localhost') } let(:compiler) { Puppet::Parser::Compiler.new(node)} let(:scope) { Puppet::Parser::Scope.new(compiler) } let(:parser) { Puppet::Pops::Parser::Parser.new() } it "should be possible to lookup a concatenated string" do scope['duck'] = 'Donald Fauntleroy Duck' expr = parser.parse_string('"Hello $duck"').current() bindings.bind.name('the_duck').to(expr) injector(lbinder).lookup(scope, 'the_duck').should == 'Hello Donald Fauntleroy Duck' end it "should be possible to post process lookup with a puppet lambda" do model = parser.parse_string('fake() |$value| {$value + 1 }').current bindings.bind.name('an_int').to(42).producer_options( :transformer => model.body.lambda) injector(lbinder).lookup(scope, 'an_int').should == 43 end it "should be possible to post process lookup with a ruby proc" do - transformer = lambda {|scope, value| value + 1 } + transformer = lambda {|value| value + 1 } bindings.bind.name('an_int').to(42).producer_options( :transformer => transformer) injector(lbinder).lookup(scope, 'an_int').should == 43 end end end context "When there are problems with configuration" do let(:lbinder) { binder.new(layered_bindings) } it "reports error for surfacing abstract bindings" do bindings.bind.abstract.name('an_int') expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/The abstract binding .* was not overridden/) end it "does not report error for abstract binding that is ovrridden" do bindings.bind.abstract.name('an_int') bindings.bind.override.name('an_int').to(142) expect{ injector(lbinder).lookup(scope, 'an_int') }.to_not raise_error end it "reports error for overriding binding that does not override" do bindings.bind.override.name('an_int').to(42) expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding with unresolved 'override' detected/) end it "reports error for binding without producer" do bindings.bind.name('an_int') expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding without producer/) end end end \ No newline at end of file diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index 42cd01eb3..99c232aa9 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1337 +1,1337 @@ 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 3x logic switches some behaviors on these even if the tests explicitly # use the 4x parser and evaluator. # Puppet[:parser] = '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.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) { @parser } let(:scope) { @scope } types = Puppet::Pops::Types::TypeFactory 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.0 == 1 " => true, "1.0 < 2 " => true, "'1.0' < 'a'" => true, "'1.0' < '' " => false, "'1.0' < ' '" => false, "'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 > 1" => /String > Integer/, "a >= 1" => /String >= Integer/, "a < 1" => /String < Integer/, "a <= 1" => /String <= Integer/, "1 > a" => /Integer > String/, "1 >= a" => /Integer >= String/, "1 < a" => /Integer < String/, "1 <= a" => /Integer <= String/, }.each do | source, error| it "should not allow comparison of String and Number '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error) 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']" => false, "'15' in [1, 0xf]" => false, "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 { "if /(ana)/ in bananas {$1}" => 'ana', "if /(xyz)/ in bananas {$1} else {$1}" => nil, "$a = bananas =~ /(ana)/; $b = /(xyz)/ in bananas; $1" => 'ana', "$a = xyz =~ /(xyz)/; $b = /(ana)/ in bananas; $1" => 'ana', "if /p/ in [pineapple, bananas] {$0}" => 'p', "if /b/ in [pineapple, bananas] {$0}" => 'b', }.each do |source, result| it "sets match variables for a regexp search using in such that '#{source}' produces '#{result}'" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Any' => ['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'" => 0, "'- 2' + '2'" => 0, '"-\t 2" + "2"' => 0, "'+2' + '2'" => 4, "'+ 2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'-2.2' + '2.2'" => 0.0, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, "'-0x2' + '0x4'" => 2, "'+0x2' + '0x4'" => 6, "'-02' + '04'" => 2, "'+02' + '04'" => 6, }.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, '"-\n 2" + "2"' => :error, '"-\v 2" + "2"' => :error, '"-2\n" + "2"' => :error, '"-2\n " + "2"' => :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, }.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]" => /attempt to assign to 'an Array Expression'/, "[a,b,c] = {b=>2,c=>3,a=>1}" => /attempt to assign to 'an Array Expression'/, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to error with #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError, result) 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 2 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 Integer', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Integer where a resource title String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Integer where a resource title 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 Integer where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Integer 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 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 = @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)) + 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 = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Any', 'lambda_arg' required_block_param end def test(lambda_arg, block) - block.call({}, lambda_arg) + 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 it 'a given undef is given as nil' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:assert_no_undef) do dispatch :assert_no_undef do param 'Any', 'x' end def assert_no_undef(x) case x when Array return unless x.include?(:undef) when Hash return unless x.keys.include?(:undef) || x.values.include?(:undef) else return unless x == :undef end raise "contains :undef" end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'assert_no_undef', the_func, __FILE__) expect{parser.evaluate_string(scope, "assert_no_undef(undef)")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef([undef])")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({undef => 1})")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({1 => undef})")}.to_not raise_error() end context 'using the 3x function api' do it 'can call a 3x function' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "bazinga(42)", __FILE__).should == 42 end it 'maps :undef to empty string' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "$a = {} bazinga($a[nope])", __FILE__).should == '' parser.evaluate_string(scope, "bazinga(undef)", __FILE__).should == '' end it 'does not map :undef to empty string in arrays' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0][0] } parser.evaluate_string(scope, "$a = {} $b = [$a[nope]] bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga([undef])", __FILE__).should == :undef end it 'does not map :undef to empty string in hashes' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0]['a'] } parser.evaluate_string(scope, "$a = {} $b = {a => $a[nope]} bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga({a => undef})", __FILE__).should == :undef end 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 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.evaluate_string(scope, source) }.to raise_error( /Illegal Resource Type expression, expected result to be a type name, or untitled Resource.*line 1:2/) 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 context 'does not leak variables' do it 'local variables are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $y SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'y'/) end it 'lambda parameters are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $x SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'x'/) end it 'does not leak match variables' do source = <<-SOURCE if 'xyz' =~ /(x)(y)(z)/ { notice $2 } case 'abc' { /(a)(b)(c)/ : { $x = $2 } } "-$x-$2-" SOURCE expect(parser.evaluate_string(scope, source)).to eq('-b--') 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