diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index bb072980a..6db8318cc 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,211 +1,239 @@ # 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) tc = Puppet::Pops::Types::TypeCalculator actual = tc.infer_set(args) if tc.callable?(type, actual) - parameters_size = parameters.size - last_captures_rest = parameters_size > 0 && parameters[-1].captures_rest args_size = args.size - unless args_size <= parameters_size || last_captures_rest - raise ArgumentError, "Too many arguments: #{args_size} for #{parameters_size}" - end - args_diff = parameters.size - args.size # associate values with parameters (NOTE: excess args for captures rest are not included in merged) - merged = parameters.zip(args.fill(:missing, args.size, args_diff)) #args) - - # calculate missing arguments - if args_diff > 0 - missing = parameters.slice(args_size, args_diff).select {|p| p.value.nil? } - unless missing.empty? - optional = parameters.count { |p| !p.value.nil? || p.captures_rest } - raise ArgumentError, "Too few arguments; #{args_size} for #{optional > 0 ? ' min ' : ''}#{parameters_size - optional}" - end - end + merged = parameters.zip(args.fill(:missing, args.size, args_diff)) - evaluated = merged.collect do |arg_assoc| + variable_bindings = {} + merged.each do |arg_assoc| # m can be one of # m = [Parameter{name => "name", value => nil], "given"] # | [Parameter{name => "name", value => Expression}, "given"] # | [Parameter{name => "name", value => Expression}, :missing] # # "given" may be nil or :undef which means that this is the value to use, # not a default expression. # # "given" is always present. If a parameter was provided then # the entry is that value, else the symbol :missing given_argument = arg_assoc[1] - argument_name = arg_assoc[0].name param_captures = arg_assoc[0].captures_rest default_expression = arg_assoc[0].value if given_argument == :missing # not given if default_expression # not given, has default value = @evaluator.evaluate(default_expression, 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 # should have been caught earlier raise Puppet::DevError, "InternalError: Should not happen! non optional parameter not caught earlier in evaluator call" end end else # given if param_captures # get excess arguments - value = args[(parameters_size-1)..-1] + value = args[(parameter_count-1)..-1] # If the input was a single nil, or undef, and there is a default, use the default 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 - # DEBATEABLE, since undef/nil selects default elsewhere (if changing, tests also needs changing). - # # Do not use given if there is a default and given is nil / undefined - # # else, let the value through - # if (given_argument.nil? || given_argument == :undef) && default_expression - # value = evaluate(default_expression, scope) - # else value = given_argument - # end end end - [argument_name, value] + + variable_bindings[arg_assoc[0].name] = value + end + + 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) + raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" end - @evaluator.evaluate_block_with_bindings(@enclosing_scope, Hash[evaluated], @model.body) + @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', actual, [self])}" end end # Call closure with argument assignment by name def call_by_name(scope, args_hash, spill_over = false) parameters = @model.parameters || [] if !spill_over && 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| scope_hash[p.name] = args_hash[p.name] || @evaluator.evaluate(p.value, scope) end missing = scope_hash.reduce([]) {|memo, entry| memo << entry[0] if entry[1].nil?; memo } unless missing.empty? optional = parameters.count { |p| !p.value.nil? } raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.join(" ,")}" end if spill_over # all args from given hash should be used, nil entries replaced by default values should win scope_hash = args_hash.merge(scope_hash) end @evaluator.evaluate_block_with_bindings(@enclosing_scope, scope_hash, @model.body) end - # incompatible with 3x except that it is an array of the same size 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 # Returns the number of optional parameters. # @return [Integer] the number of optional accepted parameters def optional_parameter_count - @model.parameters.count { |p| !p.value.nil? } + parameters.count { |p| !p.value.nil? || p.captures_rest } end # @api public def parameter_names @model.parameters.collect {|p| p.name } end # @api public def type - @callable || create_callable_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 unify_ranges(left, right) + [left[0] + right[0], left[1] + right[1]] + end + + def eval_type_expression(expression) + @evaluator.evaluate(expression, @enclosing_scope) + end + def create_callable_type() - callable_type = parameters.collect do |param| - if param.type_expr - @evaluator.evaluate(param.type_expr, @enclosing_scope) + parameter_types = parameters.collect do |param| + type = if param.type_expr + eval_type_expression(param.type_expr) + else + Puppet::Pops::Types::TypeFactory.optional_object() + 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. + range = type.size_range + type = type.element_type + elsif param.captures_rest && !type.is_a?(Puppet::Pops::Types::PArrayType) + range = ANY_NUMBER_RANGE + elsif type.is_a?(Puppet::Pops::Types::PCollectionType) + range = type.size_range + elsif param.value + range = OPTIONAL_SINGLE_RANGE else - Puppet::Pops::Types::TypeFactory.optional_object() + range = REQUIRED_SINGLE_RANGE end + + [type, range] end - callable_type << optional_parameter_count - callable_type << parameters.size + range = parameter_types.inject([0, 0]) do |sum, parameter| + unify_ranges(sum, parameter[1]) + end + + if range[1] == Puppet::Pops::Types::INFINITY + range[1] = :default + end - Puppet::Pops::Types::TypeFactory.callable(*callable_type) + Puppet::Pops::Types::TypeFactory.callable(*(parameter_types.collect(&:first) + 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/lib/puppet/pops/types/types.rb b/lib/puppet/pops/types/types.rb index 9bfe8d5e0..1545e55fb 100644 --- a/lib/puppet/pops/types/types.rb +++ b/lib/puppet/pops/types/types.rb @@ -1,506 +1,505 @@ require 'rgen/metamodel_builder' # The Types model is a model of Puppet Language types. # # The exact relationship between types is not visible in this model wrt. the PDataType which is an abstraction # of Scalar, Array[Data], and Hash[Scalar, Data] nested to any depth. This means it is not possible to # infer the type by simply looking at the inheritance hierarchy. The {Puppet::Pops::Types::TypeCalculator} should # be used to answer questions about types. The {Puppet::Pops::Types::TypeFactory} should be used to create an instance # of a type whenever one is needed. # # The implementation of the Types model contains methods that are required for the type objects to behave as # expected when comparing them and using them as keys in hashes. (No other logic is, or should be included directly in # the model's classes). # # @api public # module Puppet::Pops::Types # Used as end in a range INFINITY = 1.0 / 0.0 NEGATIVE_INFINITY = -INFINITY class PAbstractType < Puppet::Pops::Model::PopsObject abstract module ClassModule # Produce a deep copy of the type def copy Marshal.load(Marshal.dump(self)) end def hash self.class.hash end def ==(o) self.class == o.class end alias eql? == def to_s Puppet::Pops::Types::TypeCalculator.string(self) end end end # Base type for all types except {Puppet::Pops::Types::PType PType}, the type of types. # @api public class PObjectType < PAbstractType module ClassModule end end # The type of types. # @api public class PType < PObjectType contains_one_uni 'type', PAbstractType module ClassModule def hash [self.class, type].hash end def ==(o) self.class == o.class && type == o.type end end end # @api public class PNilType < PObjectType end # A flexible data type, being assignable to its subtypes as well as PArrayType and PHashType with element type assignable to PDataType. # # @api public class PDataType < PObjectType module ClassModule def ==(o) self.class == o.class || o.class == PVariantType && o == Puppet::Pops::Types::TypeCalculator.data_variant() end end end # A flexible type describing an any? of other types # @api public class PVariantType < PObjectType contains_many_uni 'types', PAbstractType, :lowerBound => 1 module ClassModule def hash [self.class, Set.new(self.types)].hash end def ==(o) (self.class == o.class && Set.new(types) == Set.new(o.types)) || (o.class == PDataType && self == Puppet::Pops::Types::TypeCalculator.data_variant()) end end end # Type that is PDataType compatible, but is not a PCollectionType. # @api public class PScalarType < PObjectType end # A string type describing the set of strings having one of the given values # class PEnumType < PScalarType has_many_attr 'values', String, :lowerBound => 1 module ClassModule def hash [self.class, Set.new(self.values)].hash end def ==(o) self.class == o.class && Set.new(values) == Set.new(o.values) end end end # @api public class PNumericType < PScalarType end # @api public class PIntegerType < PNumericType has_attr 'from', Integer, :lowerBound => 0 has_attr 'to', Integer, :lowerBound => 0 module ClassModule # The integer type is enumerable when it defines a range include Enumerable # Returns Float.Infinity if one end of the range is unbound def size return INFINITY if from.nil? || to.nil? 1+(to-from).abs end # Returns the range as an array ordered so the smaller number is always first. # The number may be Infinity or -Infinity. def range f = from || NEGATIVE_INFINITY t = to || INFINITY if f < t [f, t] else [t,f] end end # Returns Enumerator if no block is given # Returns self if size is infinity (does not yield) def each return self.to_enum unless block_given? return nil if from.nil? || to.nil? if to < from from.downto(to) {|x| yield x } else from.upto(to) {|x| yield x } end end def hash [self.class, from, to].hash end def ==(o) self.class == o.class && from == o.from && to == o.to end end end # @api public class PFloatType < PNumericType has_attr 'from', Float, :lowerBound => 0 has_attr 'to', Float, :lowerBound => 0 module ClassModule def hash [self.class, from, to].hash end def ==(o) self.class == o.class && from == o.from && to == o.to end end end # @api public class PStringType < PScalarType has_many_attr 'values', String, :lowerBound => 0, :upperBound => -1, :unique => true contains_one_uni 'size_type', PIntegerType module ClassModule def hash [self.class, self.size_type, Set.new(self.values)].hash end def ==(o) self.class == o.class && self.size_type == o.size_type && Set.new(values) == Set.new(o.values) end end end # @api public class PRegexpType < PScalarType has_attr 'pattern', String, :lowerBound => 1 has_attr 'regexp', Object, :derived => true module ClassModule def regexp_derived @_regexp = Regexp.new(pattern) unless @_regexp && @_regexp.source == pattern @_regexp end def hash [self.class, pattern].hash end def ==(o) self.class == o.class && pattern == o.pattern end end end # Represents a subtype of String that narrows the string to those matching the patterns # If specified without a pattern it is basically the same as the String type. # # @api public class PPatternType < PScalarType contains_many_uni 'patterns', PRegexpType module ClassModule def hash [self.class, Set.new(patterns)].hash end def ==(o) self.class == o.class && Set.new(patterns) == Set.new(o.patterns) end end end # @api public class PBooleanType < PScalarType end # @api public class PCollectionType < PObjectType contains_one_uni 'element_type', PAbstractType contains_one_uni 'size_type', PIntegerType module ClassModule # Returns an array with from (min) size to (max) size - # A negative range value in from is def size_range return [0, INFINITY] if size_type.nil? f = size_type.from || 0 t = size_type.to || INFINITY if f < t [f, t] else [t,f] end end def hash [self.class, element_type, size_type].hash end def ==(o) self.class == o.class && element_type == o.element_type && size_type == o.size_type end end end class PStructElement < Puppet::Pops::Model::PopsObject has_attr 'name', String, :lowerBound => 1 contains_one_uni 'type', PAbstractType module ClassModule def hash [self.class, type, name].hash end def ==(o) self.class == o.class && type == o.type && name == o.name end end end # @api public class PStructType < PObjectType contains_many_uni 'elements', PStructElement, :lowerBound => 1 has_attr 'hashed_elements', Object, :derived => true module ClassModule def hashed_elements_derived @_hashed ||= elements.reduce({}) {|memo, e| memo[e.name] = e.type; memo } @_hashed end def clear_hashed_elements @_hashed = nil end def hash [self.class, Set.new(elements)].hash end def ==(o) self.class == o.class && hashed_elements == o.hashed_elements end end end # @api public class PTupleType < PObjectType contains_many_uni 'types', PAbstractType, :lowerBound => 1 # If set, describes min and max required of the given types - if max > size of # types, the last type entry repeats # contains_one_uni 'size_type', PIntegerType, :lowerBound => 0 module ClassModule # Returns the number of elements accepted [min, max] in the tuple def size_range types_size = types.size size_type.nil? ? [types_size, types_size] : size_type.range end # Returns the number of accepted occurrences [min, max] of the last type in the tuple # The defaults is [1,1] # def repeat_last_range types_size = types.size if size_type.nil? return [1, 1] end from, to = size_type.range() min = from - (types_size-1) min = min <= 0 ? 0 : min max = to - (types_size-1) [min, max] end def hash [self.class, size_type, Set.new(types)].hash end def ==(o) self.class == o.class && types == o.types && size_type == o.size_type end end end class PCallableType < PObjectType # Types of parameters and required/optional count contains_one_uni 'param_types', PTupleType, :lowerBound => 1 # Although being an abstract type reference, only PAbstractCallable, and Optional[Callable] are supported # If not set, the meaning is that block is not supported. # contains_one_uni 'block_type', PAbstractType, :lowerBound => 0 module ClassModule # Returns the number of accepted arguments [min, max] def size_range param_types.size_range end # Returns the number of accepted arguments for the last parameter type [min, max] # def last_range param_types.repeat_last_range end # Range [0,0], [0,1], or [1,1] for the block # def block_range case block_type when Puppet::Pops::Types::POptionalType [0,1] when Puppet::Pops::Types::PVariantType, Puppet::Pops::Types::PCallableType [1,1] else [0,0] end end def hash [self.class, Set.new(param_types), block_type].hash end def ==(o) self.class == o.class && args_type == o.args_type && block_type == o.block_type end end end # @api public class PArrayType < PCollectionType module ClassModule def hash [self.class, self.element_type, self.size_type].hash end def ==(o) self.class == o.class && self.element_type == o.element_type && self.size_type == o.size_type end end end # @api public class PHashType < PCollectionType contains_one_uni 'key_type', PAbstractType module ClassModule def hash [self.class, key_type, self.element_type, self.size_type].hash end def ==(o) self.class == o.class && key_type == o.key_type && self.element_type == o.element_type && self.size_type == o.size_type end end end # @api public class PRubyType < PObjectType has_attr 'ruby_class', String module ClassModule def hash [self.class, ruby_class].hash end def ==(o) self.class == o.class && ruby_class == o.ruby_class end end end # Abstract representation of a type that can be placed in a Catalog. # @api public # class PCatalogEntryType < PObjectType end # Represents a (host-) class in the Puppet Language. # @api public # class PHostClassType < PCatalogEntryType has_attr 'class_name', String # contains_one_uni 'super_type', PHostClassType module ClassModule def hash [self.class, class_name].hash end def ==(o) self.class == o.class && class_name == o.class_name end end end # Represents a Resource Type in the Puppet Language # @api public # class PResourceType < PCatalogEntryType has_attr 'type_name', String has_attr 'title', String module ClassModule def hash [self.class, type_name, title].hash end def ==(o) self.class == o.class && type_name == o.type_name && title == o.title end end end # Represents a type that accept PNilType instead of the type parameter # required_type - is a short hand for Variant[T, Undef] # class POptionalType < PAbstractType contains_one_uni 'optional_type', PAbstractType module ClassModule def hash [self.class, optional_type].hash end def ==(o) self.class == o.class && optional_type == o.optional_type end end end end diff --git a/spec/integration/parser/future_compiler_spec.rb b/spec/integration/parser/future_compiler_spec.rb index ff52b7c03..9ffc09f74 100644 --- a/spec/integration/parser/future_compiler_spec.rb +++ b/spec/integration/parser/future_compiler_spec.rb @@ -1,704 +1,714 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/parser/parser_factory' require 'puppet_spec/compiler' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'matchers/resource' require 'rgen/metamodel_builder' # Test compilation using the future evaluator describe "Puppet::Parser::Compiler" do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = 'future' end describe "the compiler when using future parser and evaluator" do it "should be able to determine the configuration version from a local version control repository" do pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do # This should always work, because we should always be # in the puppet repo when we run this. version = %x{git rev-parse HEAD}.chomp Puppet.settings[:config_version] = 'git rev-parse HEAD' compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("testnode")) compiler.catalog.version.should == version end end it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do node = Puppet::Node.new("testnodex") node.classes = ['foo', 'bar'] catalog = compile_to_catalog(<<-PP, node) class foo { notify { foo_notify: } include bar } class bar { notify { bar_notify: } } PP catalog = Puppet::Parser::Compiler.compile(node) expect(catalog).to have_resource("Notify[foo_notify]") expect(catalog).to have_resource("Notify[bar_notify]") end it 'applies defaults for defines with qualified names (PUP-2302)' do catalog = compile_to_catalog(<<-CODE) define my::thing($msg = 'foo') { notify {'check_me': message => $msg } } My::Thing { msg => 'evoe' } my::thing { 'name': } CODE expect(catalog).to have_resource("Notify[check_me]").with_parameter(:message, "evoe") end it 'does not apply defaults from dynamic scopes (PUP-867)' do catalog = compile_to_catalog(<<-CODE) class a { Notify { message => "defaulted" } include b notify { bye: } } class b { notify { hi: } } include a CODE expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, nil) expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") end it 'gets default from inherited class (PUP-867)' do catalog = compile_to_catalog(<<-CODE) class a { Notify { message => "defaulted" } include c notify { bye: } } class b { Notify { message => "inherited" } } class c inherits b { notify { hi: } } include a CODE expect(catalog).to have_resource("Notify[hi]").with_parameter(:message, "inherited") expect(catalog).to have_resource("Notify[bye]").with_parameter(:message, "defaulted") end describe "when resolving class references" do it "should not favor local scope (with class included in topscope)" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } notify {"y" : require => Class[Experiment::Baz] } } class baz { } include baz include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end it "should not favor local scope, (with class not included in topscope)" do catalog = compile_to_catalog(<<-PP) class experiment { class baz { } notify {"x" : require => Class[Baz] } notify {"y" : require => Class[Experiment::Baz] } } class baz { } include experiment include experiment::baz PP expect(catalog).to have_resource("Notify[x]").with_parameter(:require, be_resource("Class[Baz]")) expect(catalog).to have_resource("Notify[y]").with_parameter(:require, be_resource("Class[Experiment::Baz]")) end end describe "(ticket #13349) when explicitly specifying top scope" do ["class {'::bar::baz':}", "include ::bar::baz"].each do |include| describe "with #{include}" do it "should find the top level class" do catalog = compile_to_catalog(<<-MANIFEST) class { 'foo::test': } class foo::test { #{include} } class bar::baz { notify { 'good!': } } class foo::bar::baz { notify { 'bad!': } } MANIFEST expect(catalog).to have_resource("Class[Bar::Baz]") expect(catalog).to have_resource("Notify[good!]") expect(catalog).to_not have_resource("Class[Foo::Bar::Baz]") expect(catalog).to_not have_resource("Notify[bad!]") end end end end it "should recompute the version after input files are re-parsed" do Puppet[:code] = 'class foo { }' Time.stubs(:now).returns(1) node = Puppet::Node.new('mynode') Puppet::Parser::Compiler.compile(node).version.should == 1 Time.stubs(:now).returns(2) Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change Puppet::Parser::Compiler.compile(node).version.should == 2 end ['define', 'class', 'node'].each do |thing| it "'#{thing}' is not allowed inside evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if true { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end it "'#{thing}' is not allowed inside un-evaluated conditional constructs" do expect do compile_to_catalog(<<-PP) if false { #{thing} foo { } notify { decoy: } } PP end.to raise_error(Puppet::Error, /Classes, definitions, and nodes may only appear at toplevel/) end end describe "relationships can be formed" do def extract_name(ref) ref.sub(/File\[(\w+)\]/, '\1') end def assert_creates_relationships(relationship_code, expectations) base_manifest = <<-MANIFEST file { [a,b,c]: mode => 0644, } file { [d,e]: mode => 0755, } MANIFEST catalog = compile_to_catalog(base_manifest + relationship_code) resources = catalog.resources.select { |res| res.type == 'File' } actual_relationships, actual_subscriptions = [:before, :notify].map do |relation| resources.map do |res| dependents = Array(res[relation]) dependents.map { |ref| [res.title, extract_name(ref)] } end.inject(&:concat) end actual_relationships.should =~ (expectations[:relationships] || []) actual_subscriptions.should =~ (expectations[:subscriptions] || []) end it "of regular type" do assert_creates_relationships("File[a] -> File[b]", :relationships => [['a','b']]) end it "of subscription type" do assert_creates_relationships("File[a] ~> File[b]", :subscriptions => [['a', 'b']]) end it "between multiple resources expressed as resource with multiple titles" do assert_creates_relationships("File[a,b] -> File[c,d]", :relationships => [['a', 'c'], ['b', 'c'], ['a', 'd'], ['b', 'd']]) end it "between collection expressions" do assert_creates_relationships("File <| mode == 0644 |> -> File <| mode == 0755 |>", :relationships => [['a', 'd'], ['b', 'd'], ['c', 'd'], ['a', 'e'], ['b', 'e'], ['c', 'e']]) end it "between resources expressed as Strings" do assert_creates_relationships("'File[a]' -> 'File[b]'", :relationships => [['a', 'b']]) end it "between resources expressed as variables" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = File[a] $var -> File[b] MANIFEST end it "between resources expressed as case statements" do assert_creates_relationships(<<-MANIFEST, :relationships => [['s1', 't2']]) $var = 10 case $var { 10: { file { s1: } } 12: { file { s2: } } } -> case $var + 2 { 10: { file { t1: } } 12: { file { t2: } } } MANIFEST end it "using deep access in array" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = [ [ [ File[a], File[b] ] ] ] $var[0][0][0] -> $var[0][0][1] MANIFEST end it "using deep access in hash" do assert_creates_relationships(<<-MANIFEST, :relationships => [['a', 'b']]) $var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}} $var[foo][bar][source] -> $var[foo][bar][target] MANIFEST end it "using resource declarations" do assert_creates_relationships("file { l: } -> file { r: }", :relationships => [['l', 'r']]) end it "between entries in a chain of relationships" do assert_creates_relationships("File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]", :relationships => [['a', 'b'], ['d', 'c']], :subscriptions => [['b', 'c'], ['e', 'd']]) end end context "when dealing with variable references" do it 'an initial underscore in a variable name is ok' do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $a::_a } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 10) end it 'an initial underscore in not ok if elsewhere than last segment' do expect do catalog = compile_to_catalog(<<-MANIFEST) class a { $_a = 10} include a notify { 'test': message => $_a::_a } MANIFEST end.to raise_error(/Illegal variable name/) end it 'a missing variable as default value becomes undef' do catalog = compile_to_catalog(<<-MANIFEST) class a ($b=$x) { notify {$b: message=>'meh'} } include a MANIFEST expect(catalog).to have_resource("Notify[undef]").with_parameter(:message, "meh") end end context 'when working with the trusted data hash' do context 'and have opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = true end it 'should make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => $trusted[data] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, "value") end it 'should not allow assignment to $trusted' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } expect do compile_to_catalog(<<-MANIFEST, node) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) end end context 'and have not opted in to hashed_node_data' do before :each do Puppet[:trusted_node_data] = false end it 'should not make $trusted available' do node = Puppet::Node.new("testing") node.trusted_data = { "data" => "value" } catalog = compile_to_catalog(<<-MANIFEST, node) notify { 'test': message => ($trusted == undef) } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end it 'should allow assignment to $trusted' do catalog = compile_to_catalog(<<-MANIFEST) $trusted = 'changed' notify { 'test': message => $trusted == 'changed' } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, true) end end end context 'when using typed parameters in definition' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) define foo(String $x) { } foo { 'test': x =>'say friend' } MANIFEST expect(catalog).to have_resource("Foo[test]").with_parameter(:x, 'say friend') end it 'accepts anything when parameters are untyped' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo($a, $b, $c) { } foo { 'test': a => String, b=>10, c=>undef } MANIFEST end.to_not raise_error() end it 'denies non type compliant arguments' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo(Integer $x) { } foo { 'test': x =>'say friend' } MANIFEST end.to raise_error(/type Integer, got String/) end it 'denies non type compliant default argument' do expect do catalog = compile_to_catalog(<<-MANIFEST) define foo(Integer $x = 'pow') { } foo { 'test': } MANIFEST end.to raise_error(/type Integer, got String/) end it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) define foo(Type[Bar] $x) { notify { 'test': message => $x[text] } } define bar($text) { } bar { 'joke': text => 'knock knock' } foo { 'test': x => Bar[joke] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') end end context 'when using typed parameters in class' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) class foo(String $x) { } class { 'foo': x =>'say friend' } MANIFEST expect(catalog).to have_resource("Class[Foo]").with_parameter(:x, 'say friend') end it 'accepts anything when parameters are untyped' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo($a, $b, $c) { } class { 'foo': a => String, b=>10, c=>undef } MANIFEST end.to_not raise_error() end it 'denies non type compliant arguments' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo(Integer $x) { } class { 'foo': x =>'say friend' } MANIFEST end.to raise_error(/type Integer, got String/) end it 'denies non type compliant default argument' do expect do catalog = compile_to_catalog(<<-MANIFEST) class foo(Integer $x = 'pow') { } class { 'foo': } MANIFEST end.to raise_error(/type Integer, got String/) end it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) class foo(Type[Bar] $x) { notify { 'test': message => $x[text] } } define bar($text) { } bar { 'joke': text => 'knock knock' } class { 'foo': x => Bar[joke] } MANIFEST expect(catalog).to have_resource("Notify[test]").with_parameter(:message, 'knock knock') end end context 'when using typed parameters in lambdas' do it 'accepts type compliant arguments' do catalog = compile_to_catalog(<<-MANIFEST) with('value') |String $x| { notify { "$x": } } MANIFEST expect(catalog).to have_resource("Notify[value]") end + it 'handles an array as a single argument' do + catalog = compile_to_catalog(<<-MANIFEST) + with(['value', 'second']) |$x| { notify { "${x[0]} ${x[1]}": } } + MANIFEST + expect(catalog).to have_resource("Notify[value second]") + end + it 'accepts anything when parameters are untyped' do catalog = compile_to_catalog(<<-MANIFEST) ['value', 1, true, undef].each |$x| { notify { "value: $x": } } MANIFEST expect(catalog).to have_resource("Notify[value: value]") expect(catalog).to have_resource("Notify[value: 1]") expect(catalog).to have_resource("Notify[value: true]") expect(catalog).to have_resource("Notify[value: ]") end it 'accepts type-compliant, slurped arguments' do catalog = compile_to_catalog(<<-MANIFEST) with(1, 2) |Integer *$x| { notify { "${$x[0] + $x[1]}": } } MANIFEST expect(catalog).to have_resource("Notify[3]") end it 'denies non-type-compliant arguments' do expect do compile_to_catalog(<<-MANIFEST) with(1) |String $x| { } MANIFEST end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'denies non-type-compliant, slurped arguments' do expect do compile_to_catalog(<<-MANIFEST) with(1, "hello") |Integer *$x| { } MANIFEST end.to raise_error(/called with mis-matched arguments.*expected.*Integer.*actual.*Integer, String/m) end it 'denies non-type-compliant default argument' do expect do compile_to_catalog(<<-MANIFEST) with(1) |$x, String $defaulted = 1| { notify { "${$x + $defaulted}": }} MANIFEST - end.to raise_error(/type String, got Integer/) + end.to raise_error(/expected.*Optional.*String.*actual.*Integer.*Integer/m) end it 'raises an error when a default argument value is an incorrect type and there are no arguments passed' do expect do compile_to_catalog(<<-MANIFEST) with() |String $defaulted = 1| {} MANIFEST - end.to raise_error(/type String, got Integer/) + end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'raises an error when the default argument for a slurped parameger is an incorrect type' do expect do compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = 1| {} MANIFEST - end.to raise_error(/type String, got Integer/) + end.to raise_error(/expected.*String.*actual.*Integer/m) end it 'allows using an array as the default slurped value' do catalog = compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = [hi]| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'allows using a value of the type as the default slurped value' do catalog = compile_to_catalog(<<-MANIFEST) with() |String *$defaulted = hi| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'allows specify the type of a slurped parameter as an array' do catalog = compile_to_catalog(<<-MANIFEST) with() |Array[String] *$defaulted = hi| { notify { $defaulted[0]: } } MANIFEST expect(catalog).to have_resource('Notify[hi]') end it 'raises an error when the number of default values does not match the parameter\'s size specification' do expect do compile_to_catalog(<<-MANIFEST) with() |Array[String, 2] *$defaulted = hi| { } MANIFEST - end.to raise_error(/wrong number got 1, expected at least 2/) + end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{0\}/m) end it 'raises an error when the number of passed values does not match the parameter\'s size specification' do expect do - evaluate(<<-MANIFEST) + compile_to_catalog(<<-MANIFEST) with(hi) |Array[String, 2] *$passed| { } MANIFEST - end.to raise_error(/wrong number got 1, expected at least 2/) + end.to raise_error(/expected.*arg count \{2,\}.*actual.*arg count \{1\}/m) end it 'matches when the number of arguments passed for a slurp parameter match the size specification' do catalog = compile_to_catalog(<<-MANIFEST) with(hi, bye) |Array[String, 2] *$passed| { $passed.each |$n| { notify { $n: } } } MANIFEST expect(catalog).to have_resource('Notify[hi]') expect(catalog).to have_resource('Notify[bye]') end it 'raises an error when the number of allowed slurp parameters exceeds the size constraint' do expect do compile_to_catalog(<<-MANIFEST) with(hi, bye) |Array[String, 1, 1] *$passed| { } MANIFEST - end.to raise_error(/wrong number got 2, expected at between 1 and 1/) + end.to raise_error(/expected.*arg count \{1\}.*actual.*arg count \{2\}/m) end it 'allows passing slurped arrays by specifying an array of arrays' do - catalog = compile_to_catalog(<<-MANIFEST).to + catalog = compile_to_catalog(<<-MANIFEST) with([hi], [bye]) |Array[Array[String, 1, 1]] *$passed| { notify { $passed[0][0]: } notify { $passed[1][0]: } } MANIFEST + + expect(catalog).to have_resource('Notify[hi]') + expect(catalog).to have_resource('Notify[bye]') end it 'raises an error when a required argument follows an optional one' do expect do compile_to_catalog(<<-MANIFEST) with() |$y = first, $x, Array[String, 1] *$passed = bye| {} MANIFEST end.to raise_error(/cannot have required argument after optional/) end it 'raises an error when the minimum size of a slurped argument makes it required and it follows an optional argument' do expect do compile_to_catalog(<<-MANIFEST) with() |$x = first, Array[String, 1] *$passed| {} MANIFEST end.to raise_error(/cannot have required argument after optional/) end it 'allows slurped arguments with a minimum size of 0 after an optional argument' do - catalog = compile_to_catalog(<<-MANIFEST).to - with() |$x = first, Array[String, 0] *$passed| { + catalog = compile_to_catalog(<<-MANIFEST) + with() |$x = first, Array[String, 0] *$passed| { notify { $x: } } MANIFEST expect(catalog).to have_resource('Notify[first]') end it 'accepts a Resource as a Type' do catalog = compile_to_catalog(<<-MANIFEST) define bar($text) { } bar { 'joke': text => 'knock knock' } with(Bar[joke]) |Type[Bar] $joke| { notify { "${joke[text]}": } } MANIFEST expect(catalog).to have_resource("Notify[knock knock]") end end end context 'when evaluating collection' do it 'matches on container inherited tags' do Puppet[:code] = <<-MANIFEST class xport_test { tag('foo_bar') @notify { 'nbr1': message => 'explicitly tagged', tag => 'foo_bar' } @notify { 'nbr2': message => 'implicitly tagged' } Notify <| tag == 'foo_bar' |> { message => 'overridden' } } include xport_test MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) expect(catalog).to have_resource("Notify[nbr1]").with_parameter(:message, 'overridden') expect(catalog).to have_resource("Notify[nbr2]").with_parameter(:message, 'overridden') end end end