diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index ed5899015..038b43f76 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,533 +1,511 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' 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 def [](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)[$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}" - nil - end - elsif ephemeral_include?(name) or table.include?(name) - # We can't use "if table[name]" here because the value might be false - if options[:dynamic] and self != compiler.topscope - 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 Puppet 2.8. Use a fully-qualified variable name (e.g., $classname::variable) or parameterized classes." - end - table[name] - elsif parent - parent[name,options.merge(:dynamic => (dynamic || options[:dynamic]))] - else - nil - end + lookupvar(name, options) end def []=(var, value) setvar(var, value) end # A demeterific shortcut to the catalog. def catalog compiler.catalog end def each to_hash.each { |name, value| yield(name, value) } end # Proxy accessors def host @compiler.node.name end def include?(name) ! self[name].nil? 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 : Puppet::Node::Environment.new 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) if v.nil? or v == :undef x else v end 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})) 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 + nil 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.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) else - :undefined + nil 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 + nil 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 + nil 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 define_settings(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. # It's preferred that you use self[]= instead of this; only use this # when you need to set options. 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('',self[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 # NOTE: I have watched this method end up in an infinite recursion / stack overflow. It seems # to me that we ought to be checkign with "respond_to?" before calling send, and throwing an # exception if we get a "false" back. However, I tried this, and it broke 1 test in scope_spec... # and I don't have time to debug it further right now. --cprice 2012-03-15 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) if environment != Puppet::Node::Environment.root end end diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index 65a304837..cf302340d 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -1,203 +1,203 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' describe 'function for dynamically creating resources' do include PuppetSpec::Compiler before :each do @scope = Puppet::Parser::Scope.new @scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production')) @topscope = @scope.compiler.topscope @compiler = @scope.compiler @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 it 'empty hash should not cause resources to be added' do 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 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 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 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 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 it 'should be able to create defined resoure types' do 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 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') + }.should raise_error(Puppet::Error, 'Must pass one to Foocreateresource[blah] on node foonode') end it 'should be able to add multiple defines' do 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 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 rg.path_between(test,blah).should be catalog.resource(:notify, "blah")['message'].should == 'two' end it 'should account for default values' do 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 it 'should be able to create classes' do 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 { 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 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 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 edb95d348..850b67f21 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -1,856 +1,856 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/compiler' describe Puppet::Parser::Scope do before :each do @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 = Puppet::Node::Environment.new compiler = stub 'compiler', :environment => env scope = Puppet::Parser::Scope.new :compiler => compiler scope.environment.should equal(env) end it "should use the default environment if none is available" do Puppet::Parser::Scope.new.environment.should equal(Puppet::Node::Environment.new) 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") root = Puppet::Node::Environment.root compiler = stub 'compiler', :environment => env scope = Puppet::Parser::Scope.new(:compiler => compiler) scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(env)) scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(root)) end it "should extend itself with the default Functions module if its environment is the default" do root = Puppet::Node::Environment.root scope = Puppet::Parser::Scope.new scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(root)) end end describe "when looking up a variable" do it "should support :lookupvar and :setvar for backward compatibility" do @scope.setvar("var", "yep") @scope.lookupvar("var").should == "yep" end it "should return nil for unset variables" do @scope["var"].should be_nil end it "should be able to look up values" do @scope["var"] = "yep" @scope["var"].should == "yep" end it "should be able to look up hashes" do @scope["var"] = {"a" => "b"} @scope["var"].should == {"a" => "b"} end it "should be able to look up variables in parent scopes" do @topscope["var"] = "parentval" @scope["var"].should == "parentval" end it "should prefer its own values to parent values" do @topscope["var"] = "parentval" @scope["var"] = "childval" @scope["var"].should == "childval" end it "should be able to detect when variables are set" do @scope["var"] = "childval" @scope.should be_include("var") end it "should be able to detect when variables are not set" do @scope.should_not be_include("var") end it "should support iteration over its variables" do @scope["one"] = "two" @scope["three"] = "four" hash = {} @scope.each { |name, value| hash[name] = value } hash.should == {"one" => "two", "three" => "four" } end it "should include Enumerable" do @scope.singleton_class.ancestors.should be_include(Enumerable) 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["othervar"] = "otherval" @scope["::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["var"] = "otherval" @scope["::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["var"] = "otherval" @scope["other::deep::klass::var"].should == "otherval" end it "should return nil for qualified variables that cannot be found in other classes" do other_scope = create_class_scope("other::deep::klass") @scope["other::deep::klass::var"].should be_nil end it "should warn and return nil for qualified variables whose classes have not been evaluated" do klass = newclass("other::deep::klass") @scope.expects(:warning) @scope["other::deep::klass::var"].should be_nil end it "should warn and return nil for qualified variables whose classes do not exist" do @scope.expects(:warning) @scope["other::deep::klass::var"].should be_nil end it "should return nil when asked for a non-string qualified variable from a class that does not exist" do @scope.stubs(:warning) @scope["other::deep::klass::var"].should be_nil end it "should return nil when asked for a non-string qualified variable from a class that has not been evaluated" do @scope.stubs(:warning) klass = newclass("other::deep::klass") @scope["other::deep::klass::var"].should be_nil end end end describe "when mixing inheritence and inclusion" do include PuppetSpec::Compiler def expect_the_message_to_be(message) catalog = compile_to_catalog(yield) catalog.resource('Notify', 'something')[:message].should == message end context "deprecated scoping" do before :each do - Puppet.expects(:deprecation_warning) + Puppet.expects(:deprecation_warning).at_least(1) 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 variables are set 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(:[]).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["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["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["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["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["1"].should be_nil end it "should not remove classic variables when unset_ephemeral_var is called" do @scope['myvar'] = :value1 @scope.setvar("1", :value2, :ephemeral => true) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var @scope["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["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["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["1"].should be_nil 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["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["foo"] = "bar" @scope.unsetvar("foo") @scope["foo"].should be_nil end it "should be able to unset ephemeral variables" do @scope.setvar("0", "bar", :ephemeral => true) @scope.unsetvar("0") @scope["0"].should be_nil 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["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.define_settings(: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.define_settings(:mytype, param) lambda { @scope.define_settings(: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.define_settings(:mytype, param1) param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source")) @scope.define_settings(: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.define_settings(:mytype, param1) child_scope = @scope.newscope param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source")) child_scope.define_settings(:mytype, param2) child_scope.lookupdefaults(:mytype).should == {:myparam => param1, :other => param2} end end context "#true?" do { "a string" => true, "true" => true, "false" => true, true => true, "" => false, :undef => false }.each do |input, output| it "should treat #{input.inspect} as #{output}" do Puppet::Parser::Scope.true?(input).should == output end end end end