diff --git a/acceptance/tests/language/ticket_8174_enc_causes_spurious_deprecation_warnings.rb b/acceptance/tests/language/ticket_8174_enc_causes_spurious_deprecation_warnings.rb deleted file mode 100644 index c29e5bf72..000000000 --- a/acceptance/tests/language/ticket_8174_enc_causes_spurious_deprecation_warnings.rb +++ /dev/null @@ -1,80 +0,0 @@ -test_name "#8174: incorrect warning about deprecated scoping" - -testdir = master.tmpdir('scoping_deprecation') - -create_remote_file(master, "#{testdir}/puppet.conf", < $fqdn } - notify { "locally declared var": message => $locally } - notify { "var via enc": message => $enc_var } - notify { "declared top scope": message => $top_scope } - notify { "declared node": message => $node_var } - - include a::b -} -PP -create_remote_file(master, "#{testdir}/modules/a/manifests/b.pp", <<-PP) -class a::b { - notify { "dynamic from elsewhere": message => $dynamic_for_b } -} -PP - -on master, "chown -R root:puppet #{testdir}" -on master, "chmod -R g+rwX #{testdir}" -on master, "chmod -R a+x #{testdir}/enc" -on master, "touch #{testdir}/log" -on master, "chown puppet #{testdir}/log" - -assert_log_on_master_contains = lambda do |string| - on master, "grep '#{string}' #{testdir}/log" -end - -assert_log_on_master_does_not_contain = lambda do |string| - on master, "grep -v '#{string}' #{testdir}/log" -end - -with_master_running_on(master, "--config #{testdir}/puppet.conf --debug --verbose --daemonize --dns_alt_names=\"puppet,$(hostname -s),$(hostname -f)\" --autosign true --logdest #{testdir}/log") do - agents.each do |agent| - run_agent_on(agent, "--no-daemonize --onetime --server #{master}") - end - - assert_log_on_master_contains['Dynamic lookup of $dynamic_for_b'] - assert_log_on_master_does_not_contain['Dynamic lookup of $fqdn'] - assert_log_on_master_does_not_contain['Dynamic lookup of $locally'] - assert_log_on_master_does_not_contain['Dynamic lookup of $enc_var'] - assert_log_on_master_does_not_contain['Dynamic lookup of $top_scope'] - assert_log_on_master_does_not_contain['Dynamic lookup of $node_var'] -end - -on master, "rm -rf #{testdir}" diff --git a/acceptance/tests/ticket_5027_warn_on_dynamic_scope.rb b/acceptance/tests/ticket_5027_warn_on_dynamic_scope.rb deleted file mode 100644 index a918f0788..000000000 --- a/acceptance/tests/ticket_5027_warn_on_dynamic_scope.rb +++ /dev/null @@ -1,28 +0,0 @@ -test_name "#5027: Issue warnings when using dynamic scope" - -step "Apply dynamic scoping manifest on agents" -apply_manifest_on agents, %q{ - $foo = 'foo_value' - - class a { - $bar = 'bar_value' - - include b - } - - class b inherits c { - notify { $baz: } # should not generate a warning -- inherited from class c - notify { $bar: } # should generate a warning -- uses dynamic scoping - notify { $foo: } # should not generate a warning -- comes from top scope - } - - class c { - $baz = 'baz_value' - } - - include a -} - -step "Verify deprecation warning" -fail_test "Deprecation warning not issued" unless - stdout.include? 'warning: Dynamic lookup' diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 9672a3bc8..dbc6ec10d 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,488 +1,453 @@ # 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 = {}) 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 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 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})) + qualified_scope($1).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}" 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})) + class_scope(parent_type).lookupvar(name,options.merge({:origin => nil})) elsif parent - parent.twoscope_lookupvar(name, options) + parent.lookupvar(name, options) else 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}" - 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 - nil - 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 # 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 # 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| 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) if options[:append] error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope") else error = Puppet::ParseError.new("Cannot reassign variable #{name}") end error.file = options[:file] if options[:file] error.line = options[:line] if options[:line] raise error end if options[:append] # 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 table[name].merge!(value) else raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" if table[name].is_a?(Hash) table[name] << value end else table[name] = value end end # Return the tags associated with this scope. def tags resource.tags end # Used mainly for logging def to_s "Scope(#{@resource})" 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/scope_spec.rb b/spec/unit/parser/scope_spec.rb index ee9cc7c8b..f46a4395f 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -1,836 +1,760 @@ #!/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 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 "does not allow changing a set value" do @scope["var"] = "childval" expect { @scope["var"] = "change" }.should raise_error(Puppet::Error, "Cannot reassign variable 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).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 { - } + 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 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 + } + 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 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) expect { @scope.setvar("var", "1", :append => true) }.should raise_error(Puppet::ParseError, "Cannot append, variable var is defined in this scope") 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) expect { @scope.setvar("var", "not a hash", :append => true) }.should raise_error(ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported") 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) expect { @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 expect { @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 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) expect { @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