diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 46c1f867f..569c3c823 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,491 +1,491 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'puppet/transportable' require 'strscan' require 'puppet/resource/type_collection_helper' class Puppet::Parser::Scope include Puppet::Resource::TypeCollectionHelper require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors attr_accessor :source, :resource attr_accessor :base, :keyword attr_accessor :top, :translated, :compiler attr_accessor :parent attr_reader :namespaces # thin wrapper around an ephemeral # symbol table. # when a symbol class Ephemeral def initialize(parent=nil) @symbols = {} @parent = parent end [:include?, :delete, :[]=].each do |m| define_method(m) do |*args| @symbols.send(m, *args) end end def [](name) unless @symbols.include?(name) or @parent.nil? @parent[name] else @symbols[name] end end end # A demeterific shortcut to the catalog. def catalog compiler.catalog end def environment compiler.environment end # Proxy accessors def host @compiler.node.name end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) (value != false and value != "" and value != :undef) end # Is the value a number?, return the correct object or nil if not a number def self.number?(value) return nil unless value.is_a?(Fixnum) or value.is_a?(Bignum) or value.is_a?(Float) or value.is_a?(String) if value.is_a?(String) if value =~ /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ return value.to_f elsif value =~ /^0x[0-9a-f]+$/i return value.to_i(16) elsif value =~ /^0[0-7]+$/ return value.to_i(8) elsif value =~ /^-?\d+$/ return value.to_i else return nil end end # it is one of Fixnum,Bignum or Float value end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end # Remove this when rebasing def environment compiler ? compiler.environment : nil end def find_hostclass(name) known_resource_types.find_hostclass(namespaces, name) end def find_definition(name) known_resource_types.find_definition(namespaces, name) end def findresource(string, name = nil) compiler.findresource(string, name) end # Initialize our new scope. Defaults to having no parent. def initialize(hash = {}) if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument #{name}" end } extend_with_functions_module @tags = [] # The symbol table for this scope. This is where we store variables. @symtable = {} # the ephemeral symbol tables # those should not persist long, and are used for the moment only # for $0..$xy capture variables of regexes # this is actually implemented as a stack, with each ephemeral scope # shadowing the previous one @ephemeral = [ Ephemeral.new ] # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaults = Hash.new { |dhash,type| dhash[type] = {} } # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) return parent.class_set(name,scope) if parent @class_scopes[name] = scope end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name k = klass.respond_to?(:name) ? klass.name : klass @class_scopes[k] || (parent && parent.class_scope(k)) end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents unless parent.nil? parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaults.include?(type) @defaults[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] values end # Look up a defined type. def lookuptype(name) find_definition(name) || find_hostclass(name) end def undef_as(x,v) (v == :undefined) ? x : (v == :undef) ? x : v end def qualified_scope(classname) raise "class #{classname} could not be found" unless klass = find_hostclass(classname) raise "class #{classname} has not been evaluated" unless kscope = class_scope(klass) kscope end private :qualified_scope # Look up a variable with traditional scoping and then with new scoping. If # the answers differ then print a deprecation warning. def lookupvar(name, options = {}) dynamic_value = dynamic_lookupvar(name,options) twoscope_value = twoscope_lookupvar(name,options) if dynamic_value != twoscope_value location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' Puppet.deprecation_warning "Dynamic lookup of $#{name}#{location} is deprecated. Support will be removed in a later version of Puppet. Use a fully-qualified variable name (e.g., $classname::variable) or parameterized classes." end dynamic_value end # Look up a variable. The simplest value search we do. def twoscope_lookupvar(name, options = {}) # Save the originating scope for the request options[:origin] = self unless options[:origin] table = ephemeral?(name) ? @ephemeral.last : @symtable if name =~ /^(.*)::(.+)$/ begin - qualified_scope($1).twoscope_lookupvar($2,options.merge({:origin => nil})) + qualified_scope($1).twoscope_lookupvar($2, options.merge({:origin => nil})) rescue RuntimeError => e location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' warning "Could not look up qualified variable '#{name}'; #{e.message}#{location}" :undefined end # If the value is present and either we are top/node scope or originating scope... elsif (ephemeral_include?(name) or table.include?(name)) and (compiler and self == compiler.topscope or (self.resource and self.resource.type == "Node") or self == options[:origin]) table[name] - elsif resource and resource.resource_type and resource.resource_type.respond_to?("parent") and parent_type = resource.resource_type.parent + elsif resource and resource.type == "Class" and parent_type = resource.resource_type.parent class_scope(parent_type).twoscope_lookupvar(name,options.merge({:origin => nil})) elsif parent - parent.twoscope_lookupvar(name,options) + parent.twoscope_lookupvar(name, options) else :undefined end end # Look up a variable. The simplest value search we do. def dynamic_lookupvar(name, options = {}) table = ephemeral?(name) ? @ephemeral.last : @symtable # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /^(.*)::(.+)$/ begin qualified_scope($1).dynamic_lookupvar($2,options) rescue RuntimeError => e location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' warning "Could not look up qualified variable '#{name}'; #{e.message}#{location}" :undefined end elsif ephemeral_include?(name) or table.include?(name) # We can't use "if table[name]" here because the value might be false table[name] elsif parent parent.dynamic_lookupvar(name,options) else :undefined end end # Return a hash containing our variables and their values, optionally (and # by default) including the values defined in our parent. Local values # shadow parent values. def to_hash(recursive = true) target = parent.to_hash(recursive) if recursive and parent target ||= Hash.new @symtable.keys.each { |name| value = @symtable[name] if value == :undef target.delete(name) else target[name] = value end } target end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compiler.newscope(self, options) end def parent_module_name return nil unless @parent return nil unless @parent.source @parent.source.module_name end # Return the list of scopes up to the top scope, ordered with our own first. # This is used for looking up variables and defaults. def scope_path if parent [self, parent.scope_path].flatten.compact else [self] end end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def setdefaults(type, params) table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] if table.include?(param.name) raise Puppet::ParseError.new("Default already defined for #{type} { #{param.name} }; cannot redefine", param.line, param.file) end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. def setvar(name,value, options = {}) table = options[:ephemeral] ? @ephemeral.last : @symtable if table.include?(name) unless options[:append] error = Puppet::ParseError.new("Cannot reassign variable #{name}") else error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope") end error.file = options[:file] if options[:file] error.line = options[:line] if options[:line] raise error end unless options[:append] table[name] = value else # append case # lookup the value in the scope if it exists and insert the var table[name] = undef_as('',lookupvar(name)) # concatenate if string, append if array, nothing for other types case value when Array table[name] += value when Hash raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" unless value.is_a?(Hash) table[name].merge!(value) else table[name] << value end end end # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags resource.tags end # Used mainly for logging def to_s "Scope(#{@resource})" end # Undefine a variable; only used for testing. def unsetvar(var) table = ephemeral?(var) ? @ephemeral.last : @symtable table.delete(var) if table.include?(var) end # remove ephemeral scope up to level def unset_ephemeral_var(level=:all) if level == :all @ephemeral = [ Ephemeral.new ] else (@ephemeral.size - level).times do @ephemeral.pop end end end # check if name exists in one of the ephemeral scope. def ephemeral_include?(name) @ephemeral.reverse_each do |eph| return true if eph.include?(name) end false end # is name an ephemeral variable? def ephemeral?(name) name =~ /^\d+$/ end def ephemeral_level @ephemeral.size end def new_ephemeral @ephemeral.push(Ephemeral.new(@ephemeral.last)) end def ephemeral_from(match, file = nil, line = nil) raise(ArgumentError,"Invalid regex match data") unless match.is_a?(MatchData) new_ephemeral setvar("0", match[0], :file => file, :line => line, :ephemeral => true) match.captures.each_with_index do |m,i| setvar("#{i+1}", m, :file => file, :line => line, :ephemeral => true) end end def find_resource_type(type) # It still works fine without the type == 'class' short-cut, but it is a lot slower. return nil if ["class", "node"].include? type.to_s.downcase find_builtin_resource_type(type) || find_defined_resource_type(type) end def find_builtin_resource_type(type) Puppet::Type.type(type.to_s.downcase.to_sym) end def find_defined_resource_type(type) environment.known_resource_types.find_definition(namespaces, type.to_s.downcase) end def method_missing(method, *args, &block) method.to_s =~ /^function_(.*)$/ super unless $1 super unless Puppet::Parser::Functions.function($1) # Calling .function(name) adds "function_#{name}" as a callable method on # self if it's found, so now we can just send it send(method, *args) end def resolve_type_and_titles(type, titles) raise ArgumentError, "titles must be an array" unless titles.is_a?(Array) case type.downcase when "class" # resolve the titles titles = titles.collect do |a_title| hostclass = find_hostclass(a_title) hostclass ? hostclass.name : a_title end when "node" # no-op else # resolve the type resource_type = find_resource_type(type) type = resource_type.name if resource_type end return [type, titles] end private def extend_with_functions_module extend Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.root) extend Puppet::Parser::Functions.environment_module(environment) end end diff --git a/spec/lib/puppet_spec/compiler.rb b/spec/lib/puppet_spec/compiler.rb new file mode 100644 index 000000000..54741c6e9 --- /dev/null +++ b/spec/lib/puppet_spec/compiler.rb @@ -0,0 +1,6 @@ +module PuppetSpec::Compiler + def compile_to_catalog(string) + Puppet[:code] = string + Puppet::Parser::Compiler.compile(Puppet::Node.new('foonode')) + end +end diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index 8d4d5d855..65a304837 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -1,171 +1,203 @@ require 'puppet' require 'spec_helper' +require 'puppet_spec/compiler' describe 'function for dynamically creating resources' do + include PuppetSpec::Compiler - def get_scope - @topscope = Puppet::Parser::Scope.new - # This is necessary so we don't try to use the compiler to discover our parent. - @topscope.parent = nil + before :each do @scope = Puppet::Parser::Scope.new @scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production')) - @scope.parent = @topscope + @topscope = @scope.compiler.topscope @compiler = @scope.compiler - end - before :each do - get_scope + @scope.parent = @topscope Puppet::Parser::Functions.function(:create_resources) end it "should exist" do Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources" end it 'should require two or three arguments' do expect { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2 or 3)') expect { @scope.function_create_resources(['foo', 'bar', 'blah', 'baz']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (4; must be 2 or 3)') end describe 'when the caller does not supply a name parameter' do it 'should set a default resource name equal to the resource title' do Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').once @scope.function_create_resources(['notify', {'test'=>{}}]) end end + describe 'when the caller supplies a name parameter' do it 'should set the resource name to the value provided' do Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'user_supplied').once Puppet::Parser::Resource.any_instance.expects(:set_parameter).with(:name, 'test').never @scope.function_create_resources(['notify', {'test'=>{'name' => 'user_supplied'}}]) end end describe 'when creating native types' do - before :each do - Puppet[:code]='notify{test:}' - get_scope - @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) - end - it 'empty hash should not cause resources to be added' do - @scope.function_create_resources(['file', {}]) - @compiler.catalog.resources.size == 1 + noop_catalog = compile_to_catalog("create_resources('file', {})") + empty_catalog = compile_to_catalog("") + noop_catalog.resources.size.should == empty_catalog.resources.size end + it 'should be able to add' do - @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}}]) - @compiler.catalog.resource(:file, "/etc/foo")['ensure'].should == 'present' + catalog = compile_to_catalog("create_resources('file', {'/etc/foo'=>{'ensure'=>'present'}})") + catalog.resource(:file, "/etc/foo")['ensure'].should == 'present' end + it 'should accept multiple types' do - type_hash = {} - type_hash['foo'] = {'message' => 'one'} - type_hash['bar'] = {'message' => 'two'} - @scope.function_create_resources(['notify', type_hash]) - @compiler.catalog.resource(:notify, "foo")['message'].should == 'one' - @compiler.catalog.resource(:notify, "bar")['message'].should == 'two' + catalog = compile_to_catalog("create_resources('notify', {'foo'=>{'message'=>'one'}, 'bar'=>{'message'=>'two'}})") + catalog.resource(:notify, "foo")['message'].should == 'one' + catalog.resource(:notify, "bar")['message'].should == 'two' end + it 'should fail to add non-existing type' do expect { @scope.function_create_resources(['create-resource-foo', {}]) }.should raise_error(ArgumentError, 'could not create resource of unknown type create-resource-foo') end + it 'should be able to add edges' do - @scope.function_create_resources(['notify', {'foo'=>{'require' => 'Notify[test]'}}]) - @scope.compiler.compile - rg = @scope.compiler.catalog.to_ral.relationship_graph + catalog = compile_to_catalog("notify { test: }\n create_resources('notify', {'foo'=>{'require'=>'Notify[test]'}})") + rg = catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } foo = rg.vertices.find { |v| v.title == 'foo' } test.should be foo.should be rg.path_between(test,foo).should be end + it 'should account for default values' do - @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}, '/etc/baz'=>{'group'=>'food'}}, {'group' => 'bar'}]) - @compiler.catalog.resource(:file, "/etc/foo")['group'].should == 'bar' - @compiler.catalog.resource(:file, "/etc/baz")['group'].should == 'food' + catalog = compile_to_catalog("create_resources('file', {'/etc/foo'=>{'ensure'=>'present'}, '/etc/baz'=>{'group'=>'food'}}, {'group' => 'bar'})") + catalog.resource(:file, "/etc/foo")['group'].should == 'bar' + catalog.resource(:file, "/etc/baz")['group'].should == 'food' end end describe 'when dynamically creating resource types' do - before :each do - Puppet[:code]= -'define foocreateresource($one){notify{$name: message => $one}} -notify{test:} -' - get_scope - @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) - Puppet::Parser::Functions.function(:create_resources) - end it 'should be able to create defined resoure types' do - @scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two'}}]) - # still have to compile for this to work... - # I am not sure if this constraint ruins the tests - @scope.compiler.compile - @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' + catalog = compile_to_catalog(<<-MANIFEST) + define foocreateresource($one) { + notify { $name: message => $one } + } + + create_resources('foocreateresource', {'blah'=>{'one'=>'two'}}) + MANIFEST + catalog.resource(:notify, "blah")['message'].should == 'two' end + it 'should fail if defines are missing params' do - @scope.function_create_resources(['foocreateresource', {'blah'=>{}}]) - expect { @scope.compiler.compile }.should raise_error(Puppet::ParseError, 'Must pass one to Foocreateresource[blah] at line 1') + expect { + compile_to_catalog(<<-MANIFEST) + define foocreateresource($one) { + notify { $name: message => $one } + } + + create_resources('foocreateresource', {'blah'=>{}}) + MANIFEST + }.should raise_error(Puppet::Error, 'Must pass one to Foocreateresource[blah] at line 1 on node foonode') end + it 'should be able to add multiple defines' do - hash = {} - hash['blah'] = {'one' => 'two'} - hash['blaz'] = {'one' => 'three'} - @scope.function_create_resources(['foocreateresource', hash]) - # still have to compile for this to work... - # I am not sure if this constraint ruins the tests - @scope.compiler.compile - @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' - @compiler.catalog.resource(:notify, "blaz")['message'].should == 'three' + catalog = compile_to_catalog(<<-MANIFEST) + define foocreateresource($one) { + notify { $name: message => $one } + } + + create_resources('foocreateresource', {'blah'=>{'one'=>'two'}, 'blaz'=>{'one'=>'three'}}) + MANIFEST + + catalog.resource(:notify, "blah")['message'].should == 'two' + catalog.resource(:notify, "blaz")['message'].should == 'three' end + it 'should be able to add edges' do - @scope.function_create_resources(['foocreateresource', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}]) - @scope.compiler.compile - rg = @scope.compiler.catalog.to_ral.relationship_graph + catalog = compile_to_catalog(<<-MANIFEST) + define foocreateresource($one) { + notify { $name: message => $one } + } + + notify { test: } + + create_resources('foocreateresource', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}) + MANIFEST + + rg = catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } blah = rg.vertices.find { |v| v.title == 'blah' } test.should be blah.should be - # (Yoda speak like we do) rg.path_between(test,blah).should be - @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' + catalog.resource(:notify, "blah")['message'].should == 'two' end + it 'should account for default values' do - @scope.function_create_resources(['foocreateresource', {'blah'=>{}}, {'one' => 'two'}]) - @scope.compiler.compile - @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' + catalog = compile_to_catalog(<<-MANIFEST) + define foocreateresource($one) { + notify { $name: message => $one } + } + + create_resources('foocreateresource', {'blah'=>{}}, {'one' => 'two'}) + MANIFEST + + catalog.resource(:notify, "blah")['message'].should == 'two' end end + describe 'when creating classes' do - before :each do - Puppet[:code]= -'class bar($one){notify{test: message => $one}} -notify{tester:} -' - get_scope - @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) - Puppet::Parser::Functions.function(:create_resources) - end it 'should be able to create classes' do - @scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}]) - @scope.compiler.compile - @compiler.catalog.resource(:notify, "test")['message'].should == 'two' - @compiler.catalog.resource(:class, "bar").should_not be_nil + catalog = compile_to_catalog(<<-MANIFEST) + class bar($one) { + notify { test: message => $one } + } + + create_resources('class', {'bar'=>{'one'=>'two'}}) + MANIFEST + + catalog.resource(:notify, "test")['message'].should == 'two' + catalog.resource(:class, "bar").should_not be_nil end + it 'should fail to create non-existing classes' do - expect { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah') + expect { + compile_to_catalog(<<-MANIFEST) + create_resources('class', {'blah'=>{'one'=>'two'}}) + MANIFEST + }.should raise_error(Puppet::Error ,'could not find hostclass blah at line 1 on node foonode') end + it 'should be able to add edges' do - @scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}]) - @scope.compiler.compile - rg = @scope.compiler.catalog.to_ral.relationship_graph + catalog = compile_to_catalog(<<-MANIFEST) + class bar($one) { + notify { test: message => $one } + } + + notify { tester: } + + create_resources('class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}) + MANIFEST + + rg = catalog.to_ral.relationship_graph test = rg.vertices.find { |v| v.title == 'test' } tester = rg.vertices.find { |v| v.title == 'tester' } test.should be tester.should be rg.path_between(tester,test).should be end + it 'should account for default values' do - @scope.function_create_resources(['class', {'bar'=>{}}, {'one' => 'two'}]) - @scope.compiler.compile - @compiler.catalog.resource(:notify, "test")['message'].should == 'two' - @compiler.catalog.resource(:class, "bar").should_not be_nil + catalog = compile_to_catalog(<<-MANIFEST) + class bar($one) { + notify { test: message => $one } + } + + create_resources('class', {'bar'=>{}}, {'one' => 'two'}) + MANIFEST + + catalog.resource(:notify, "test")['message'].should == 'two' + catalog.resource(:class, "bar").should_not be_nil end end end diff --git a/spec/unit/parser/scope_spec.rb b/spec/unit/parser/scope_spec.rb index 01dfcd300..4f9fbdd2d 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -1,773 +1,821 @@ #!/usr/bin/env rspec require 'spec_helper' +require 'puppet_spec/compiler' describe Puppet::Parser::Scope do before :each do - # This is necessary so we don't try to use the compiler to discover our parent. @scope = Puppet::Parser::Scope.new @scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo")) @scope.source = Puppet::Resource::Type.new(:node, :foo) @topscope = @scope.compiler.topscope @scope.parent = @topscope end it "should be able to store references to class scopes" do lambda { @scope.class_set "myname", "myscope" }.should_not raise_error end it "should be able to retrieve class scopes by name" do @scope.class_set "myname", "myscope" @scope.class_scope("myname").should == "myscope" end it "should be able to retrieve class scopes by object" do klass = mock 'ast_class' klass.expects(:name).returns("myname") @scope.class_set "myname", "myscope" @scope.class_scope(klass).should == "myscope" end it "should be able to retrieve its parent module name from the source of its parent type" do @topscope.source = Puppet::Resource::Type.new(:hostclass, :foo, :module_name => "foo") @scope.parent_module_name.should == "foo" end it "should return a nil parent module name if it has no parent" do @topscope.parent_module_name.should be_nil end it "should return a nil parent module name if its parent has no source" do @scope.parent_module_name.should be_nil end it "should get its environment from its compiler" do env = stub 'environment' compiler = stub 'compiler', :environment => env scope = Puppet::Parser::Scope.new :compiler => compiler scope.environment.should equal(env) end it "should use the resource type collection helper to find its known resource types" do Puppet::Parser::Scope.ancestors.should include(Puppet::Resource::TypeCollectionHelper) end describe "when missing methods are called" do before :each do @env = Puppet::Node::Environment.new('testing') @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new('foo', :environment => @env)) @scope = Puppet::Parser::Scope.new(:compiler => @compiler) end it "should load and call the method if it looks like a function and it exists" do @scope.function_sprintf(["%b", 123]).should == "1111011" end it "should raise NoMethodError if the method doesn't look like a function" do expect { @scope.sprintf(["%b", 123]) }.should raise_error(NoMethodError) end it "should raise NoMethodError if the method looks like a function but doesn't exist" do expect { @scope.function_fake_bs(['cows']) }.should raise_error(NoMethodError) end end describe "when initializing" do it "should extend itself with its environment's Functions module as well as the default" do env = Puppet::Node::Environment.new("myenv") compiler = stub 'compiler', :environment => env mod = Module.new root_mod = Module.new Puppet::Parser::Functions.expects(:environment_module).with(Puppet::Node::Environment.root).returns root_mod Puppet::Parser::Functions.expects(:environment_module).with(env).returns mod Puppet::Parser::Scope.new(:compiler => compiler).singleton_class.ancestors.should be_include(mod) end it "should extend itself with the default Functions module if it has no environment" do mod = Module.new Puppet::Parser::Functions.expects(:environment_module).with(Puppet::Node::Environment.root).returns(mod) Puppet::Parser::Functions.expects(:environment_module).with(nil).returns mod Puppet::Parser::Scope.new.singleton_class.ancestors.should be_include(mod) end end describe "when looking up a variable" do it "should return ':undefined' for unset variables" do @scope.lookupvar("var").should == :undefined end it "should be able to look up values" do @scope.setvar("var", "yep") @scope.lookupvar("var").should == "yep" end it "should be able to look up hashes" do @scope.setvar("var", {"a" => "b"}) @scope.lookupvar("var").should == {"a" => "b"} end it "should be able to look up variables in parent scopes" do @topscope.setvar("var", "parentval") @scope.lookupvar("var").should == "parentval" end it "should prefer its own values to parent values" do @topscope.setvar("var", "parentval") @scope.setvar("var", "childval") @scope.lookupvar("var").should == "childval" end it "should be able to look up intermediary variables in parent scopes (DEPRECATED)" do Puppet.expects(:deprecation_warning) thirdscope = Puppet::Parser::Scope.new thirdscope.parent = @scope thirdscope.source = Puppet::Resource::Type.new(:hostclass, :foo, :module_name => "foo") @scope.source = Puppet::Resource::Type.new(:hostclass, :bar, :module_name => "bar") @topscope.setvar("var2","parentval") @scope.setvar("var2","childval") thirdscope.lookupvar("var2").should == "childval" end describe "and the variable is qualified" do before :each do @known_resource_types = @scope.known_resource_types end def newclass(name) @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name) end def create_class_scope(name) klass = newclass(name) catalog = Puppet::Resource::Catalog.new catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => Puppet::Parser::Scope.new)) Puppet::Parser::Resource.new("class", name, :scope => @scope, :source => mock('source'), :catalog => catalog).evaluate @scope.class_scope(klass) end it "should be able to look up explicitly fully qualified variables from main" do Puppet.expects(:deprecation_warning).never other_scope = create_class_scope("") other_scope.setvar("othervar", "otherval") @scope.lookupvar("::othervar").should == "otherval" end it "should be able to look up explicitly fully qualified variables from other scopes" do Puppet.expects(:deprecation_warning).never other_scope = create_class_scope("other") other_scope.setvar("var", "otherval") @scope.lookupvar("::other::var").should == "otherval" end it "should be able to look up deeply qualified variables" do Puppet.expects(:deprecation_warning).never other_scope = create_class_scope("other::deep::klass") other_scope.setvar("var", "otherval") @scope.lookupvar("other::deep::klass::var").should == "otherval" end it "should return ':undefined' for qualified variables that cannot be found in other classes" do Puppet.expects(:deprecation_warning).never other_scope = create_class_scope("other::deep::klass") @scope.lookupvar("other::deep::klass::var").should == :undefined end it "should warn and return ':undefined' for qualified variables whose classes have not been evaluated" do Puppet.expects(:deprecation_warning).never klass = newclass("other::deep::klass") @scope.expects(:warning).at_least_once @scope.lookupvar("other::deep::klass::var").should == :undefined end it "should warn and return ':undefined' for qualified variables whose classes do not exist" do Puppet.expects(:deprecation_warning).never @scope.expects(:warning).at_least_once @scope.lookupvar("other::deep::klass::var").should == :undefined end it "should return ':undefined' when asked for a non-string qualified variable from a class that does not exist" do Puppet.expects(:deprecation_warning).never @scope.stubs(:warning) @scope.lookupvar("other::deep::klass::var").should == :undefined end it "should return ':undefined' when asked for a non-string qualified variable from a class that has not been evaluated" do Puppet.expects(:deprecation_warning).never @scope.stubs(:warning) klass = newclass("other::deep::klass") @scope.lookupvar("other::deep::klass::var").should == :undefined end end end describe "when mixing inheritence and inclusion" do - let(:catalog) { Puppet::Parser::Compiler.compile(Puppet::Node.new('foonode')) } + include PuppetSpec::Compiler def expect_the_message_to_be(message) - Puppet[:code] = yield - catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new('foonode')) + catalog = compile_to_catalog(yield) catalog.resource('Notify', 'something')[:message].should == message end context "deprecated scoping" do before :each do Puppet.expects(:deprecation_warning) end it "prefers values in its included scope over those from the node (DEPRECATED)" do expect_the_message_to_be('baz_msg') do <<-MANIFEST node default { $var = "node_msg" include foo } class baz { $var = "baz_msg" include bar } class foo inherits baz { } class bar { notify { 'something': message => $var, } } MANIFEST end end it "finds values in its included scope (DEPRECATED)" do expect_the_message_to_be('baz_msg') do <<-MANIFEST node default { include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { $var = "baz_msg" include bar } MANIFEST end end it "recognizes a dynamically scoped boolean (DEPRECATED)" do expect_the_message_to_be(true) do <<-MANIFEST node default { $var = false include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { $var = true include bar } MANIFEST end end end context "supported scoping" do before :each do Puppet.expects(:deprecation_warning).never end + it "finds value define in the inherited node" do + expect_the_message_to_be('parent_msg') do <<-MANIFEST + $var = "top_msg" + node parent { + $var = "parent_msg" + } + node default inherits parent { + include foo + } + class foo { + notify { 'something': message => $var, } + } + MANIFEST + end + end + + it "finds top scope when the class is included before the node defines the var" do + expect_the_message_to_be('top_msg') do <<-MANIFEST + $var = "top_msg" + node parent { + include foo + } + node default inherits parent { + $var = "default_msg" + } + class foo { + notify { 'something': message => $var, } + } + MANIFEST + end + end + + it "finds top scope when the class is included before the node defines the var" do + expect_the_message_to_be('top_msg') do <<-MANIFEST + $var = "top_msg" + node parent { + include foo + } + node default inherits parent { + $var = "default_msg" + } + class foo { + notify { 'something': message => $var, } + } + MANIFEST + end + end + + it "should find values in its local scope" do expect_the_message_to_be('local_msg') do <<-MANIFEST node default { include baz } class foo { } class bar inherits foo { $var = "local_msg" notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "should find values in its inherited scope" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers values in its inherited scope over those in the node (with intermediate inclusion)" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { $var = "node_msg" include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers values in its inherited scope over those in the node (without intermediate inclusion)" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { $var = "node_msg" include bar } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } MANIFEST end end it "prefers values in its inherited scope over those from where it is included" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { $var = "baz_msg" include bar } MANIFEST end end it "does not used variables from classes included in the inherited scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include bar } class quux { $var = "quux_msg" } class foo inherits quux { } class baz { include foo } class bar inherits baz { notify { 'something': message => $var, } } MANIFEST end end it "does not use a variable from a scope lexically enclosing it" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include other::bar } class other { $var = "other_msg" class bar { notify { 'something': message => $var, } } } MANIFEST end end it "finds values in its node scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "finds values in its top scope" do expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" node default { include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers variables from the node over those in the top scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST $var = "top_msg" node default { $var = "node_msg" include foo } class foo { notify { 'something': message => $var, } } MANIFEST end end end end describe "when setvar is called with append=true" do it "should raise error if the variable is already defined in this scope" do @scope.setvar("var","1", :append => false) lambda { @scope.setvar("var","1", :append => true) }.should raise_error(Puppet::ParseError) end it "should lookup current variable value" do @scope.expects(:lookupvar).with("var").returns("2") @scope.setvar("var","1", :append => true) end it "should store the concatenated string '42'" do @topscope.setvar("var","4", :append => false) @scope.setvar("var","2", :append => true) @scope.lookupvar("var").should == "42" end it "should store the concatenated array [4,2]" do @topscope.setvar("var",[4], :append => false) @scope.setvar("var",[2], :append => true) @scope.lookupvar("var").should == [4,2] end it "should store the merged hash {a => b, c => d}" do @topscope.setvar("var",{"a" => "b"}, :append => false) @scope.setvar("var",{"c" => "d"}, :append => true) @scope.lookupvar("var").should == {"a" => "b", "c" => "d"} end it "should raise an error when appending a hash with something other than another hash" do @topscope.setvar("var",{"a" => "b"}, :append => false) lambda { @scope.setvar("var","not a hash", :append => true) }.should raise_error end end describe "when calling number?" do it "should return nil if called with anything not a number" do Puppet::Parser::Scope.number?([2]).should be_nil end it "should return a Fixnum for a Fixnum" do Puppet::Parser::Scope.number?(2).should be_an_instance_of(Fixnum) end it "should return a Float for a Float" do Puppet::Parser::Scope.number?(2.34).should be_an_instance_of(Float) end it "should return 234 for '234'" do Puppet::Parser::Scope.number?("234").should == 234 end it "should return nil for 'not a number'" do Puppet::Parser::Scope.number?("not a number").should be_nil end it "should return 23.4 for '23.4'" do Puppet::Parser::Scope.number?("23.4").should == 23.4 end it "should return 23.4e13 for '23.4e13'" do Puppet::Parser::Scope.number?("23.4e13").should == 23.4e13 end it "should understand negative numbers" do Puppet::Parser::Scope.number?("-234").should == -234 end it "should know how to convert exponential float numbers ala '23e13'" do Puppet::Parser::Scope.number?("23e13").should == 23e13 end it "should understand hexadecimal numbers" do Puppet::Parser::Scope.number?("0x234").should == 0x234 end it "should understand octal numbers" do Puppet::Parser::Scope.number?("0755").should == 0755 end it "should return nil on malformed integers" do Puppet::Parser::Scope.number?("0.24.5").should be_nil end it "should convert strings with leading 0 to integer if they are not octal" do Puppet::Parser::Scope.number?("0788").should == 788 end it "should convert strings of negative integers" do Puppet::Parser::Scope.number?("-0788").should == -788 end it "should return nil on malformed hexadecimal numbers" do Puppet::Parser::Scope.number?("0x89g").should be_nil end end describe "when using ephemeral variables" do it "should store the variable value" do @scope.setvar("1", :value, :ephemeral => true) @scope.lookupvar("1").should == :value end it "should remove the variable value when unset_ephemeral_var is called" do @scope.setvar("1", :value, :ephemeral => true) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var @scope.lookupvar("1").should == :undefined end it "should not remove classic variables when unset_ephemeral_var is called" do @scope.setvar("myvar", :value1) @scope.setvar("1", :value2, :ephemeral => true) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var @scope.lookupvar("myvar").should == :value1 end it "should raise an error when setting it again" do @scope.setvar("1", :value2, :ephemeral => true) lambda { @scope.setvar("1", :value3, :ephemeral => true) }.should raise_error end it "should declare ephemeral number only variable names" do @scope.ephemeral?("0").should be_true end it "should not declare ephemeral other variable names" do @scope.ephemeral?("abc0").should be_nil end describe "with more than one level" do it "should prefer latest ephemeral scopes" do @scope.setvar("0", :earliest, :ephemeral => true) @scope.new_ephemeral @scope.setvar("0", :latest, :ephemeral => true) @scope.lookupvar("0").should == :latest end it "should be able to report the current level" do @scope.ephemeral_level.should == 1 @scope.new_ephemeral @scope.ephemeral_level.should == 2 end it "should check presence of an ephemeral variable accross multiple levels" do @scope.new_ephemeral @scope.setvar("1", :value1, :ephemeral => true) @scope.new_ephemeral @scope.setvar("0", :value2, :ephemeral => true) @scope.new_ephemeral @scope.ephemeral_include?("1").should be_true end it "should return false when an ephemeral variable doesn't exist in any ephemeral scope" do @scope.new_ephemeral @scope.setvar("1", :value1, :ephemeral => true) @scope.new_ephemeral @scope.setvar("0", :value2, :ephemeral => true) @scope.new_ephemeral @scope.ephemeral_include?("2").should be_false end it "should get ephemeral values from earlier scope when not in later" do @scope.setvar("1", :value1, :ephemeral => true) @scope.new_ephemeral @scope.setvar("0", :value2, :ephemeral => true) @scope.lookupvar("1").should == :value1 end describe "when calling unset_ephemeral_var without a level" do it "should remove all the variables values" do @scope.setvar("1", :value1, :ephemeral => true) @scope.new_ephemeral @scope.setvar("1", :value2, :ephemeral => true) @scope.unset_ephemeral_var @scope.lookupvar("1").should == :undefined end end describe "when calling unset_ephemeral_var with a level" do it "should remove ephemeral scopes up to this level" do @scope.setvar("1", :value1, :ephemeral => true) @scope.new_ephemeral @scope.setvar("1", :value2, :ephemeral => true) @scope.new_ephemeral @scope.setvar("1", :value3, :ephemeral => true) @scope.unset_ephemeral_var(2) @scope.lookupvar("1").should == :value2 end end end end describe "when setting ephemeral vars from matches" do before :each do @match = stub 'match', :is_a? => true @match.stubs(:[]).with(0).returns("this is a string") @match.stubs(:captures).returns([]) @scope.stubs(:setvar) end it "should accept only MatchData" do lambda { @scope.ephemeral_from("match") }.should raise_error end it "should set $0 with the full match" do @scope.expects(:setvar).with { |*arg| arg[0] == "0" and arg[1] == "this is a string" and arg[2][:ephemeral] } @scope.ephemeral_from(@match) end it "should set every capture as ephemeral var" do @match.stubs(:captures).returns([:capture1,:capture2]) @scope.expects(:setvar).with { |*arg| arg[0] == "1" and arg[1] == :capture1 and arg[2][:ephemeral] } @scope.expects(:setvar).with { |*arg| arg[0] == "2" and arg[1] == :capture2 and arg[2][:ephemeral] } @scope.ephemeral_from(@match) end it "should create a new ephemeral level" do @scope.expects(:new_ephemeral) @scope.ephemeral_from(@match) end end describe "when unsetting variables" do it "should be able to unset normal variables" do @scope.setvar("foo", "bar") @scope.unsetvar("foo") @scope.lookupvar("foo").should == :undefined end it "should be able to unset ephemeral variables" do @scope.setvar("0", "bar", :ephemeral => true) @scope.unsetvar("0") @scope.lookupvar("0").should == :undefined end it "should not unset ephemeral variables in previous ephemeral scope" do @scope.setvar("0", "bar", :ephemeral => true) @scope.new_ephemeral @scope.unsetvar("0") @scope.lookupvar("0").should == "bar" end end it "should use its namespaces to find hostclasses" do klass = @scope.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "a::b::c") @scope.add_namespace "a::b" @scope.find_hostclass("c").should equal(klass) end it "should use its namespaces to find definitions" do define = @scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "a::b::c") @scope.add_namespace "a::b" @scope.find_definition("c").should equal(define) end describe "when managing defaults" do it "should be able to set and lookup defaults" do param = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source")) @scope.setdefaults(:mytype, param) @scope.lookupdefaults(:mytype).should == {:myparam => param} end it "should fail if a default is already defined and a new default is being defined" do param = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source")) @scope.setdefaults(:mytype, param) lambda { @scope.setdefaults(:mytype, param) }.should raise_error(Puppet::ParseError) end it "should return multiple defaults at once" do param1 = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source")) @scope.setdefaults(:mytype, param1) param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source")) @scope.setdefaults(:mytype, param2) @scope.lookupdefaults(:mytype).should == {:myparam => param1, :other => param2} end it "should look up defaults defined in parent scopes" do param1 = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source")) @scope.setdefaults(:mytype, param1) child_scope = @scope.newscope param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source")) child_scope.setdefaults(:mytype, param2) child_scope.lookupdefaults(:mytype).should == {:myparam => param1, :other => param2} end end end