require 'spec_helper' require 'puppet_spec/compiler' require 'puppet_spec/scope' describe Puppet::Parser::Scope do include PuppetSpec::Scope before :each do @scope = Puppet::Parser::Scope.new( Puppet::Parser::Compiler.new(Puppet::Node.new("foo")) ) @scope.source = Puppet::Resource::Type.new(:node, :foo) @topscope = @scope.compiler.topscope @scope.parent = @topscope end describe "create_test_scope_for_node" do let(:node_name) { "node_name_foo" } let(:scope) { create_test_scope_for_node(node_name) } it "should be a kind of Scope" do expect(scope).to be_a_kind_of(Puppet::Parser::Scope) end it "should set the source to a node resource" do expect(scope.source).to be_a_kind_of(Puppet::Resource::Type) end it "should have a compiler" do expect(scope.compiler).to be_a_kind_of(Puppet::Parser::Compiler) end it "should set the parent to the compiler topscope" do expect(scope.parent).to be(scope.compiler.topscope) end end it "should generate a simple string when inspecting a scope" do expect(@scope.inspect).to eq("Scope()") end it "should generate a simple string when inspecting a scope with a resource" do @scope.resource="foo::bar" expect(@scope.inspect).to eq("Scope(foo::bar)") end it "should generate a path if there is one on the puppet stack" do result = Puppet::Pops::PuppetStack.stack('/tmp/kansas.pp', 42, @scope, 'inspect', []) expect(result).to eq("Scope(/tmp/kansas.pp, 42)") end it "should generate an <env> shortened path if path points into the environment" do env_path = @scope.environment.configuration.path_to_env mocked_path = File.join(env_path, 'oz.pp') result = Puppet::Pops::PuppetStack.stack(mocked_path, 42, @scope, 'inspect', []) expect(result).to eq("Scope(<env>/oz.pp, 42)") end it "should generate a <module> shortened path if path points into a module" do mocked_path = File.join(@scope.environment.full_modulepath[0], 'mymodule', 'oz.pp') result = Puppet::Pops::PuppetStack.stack(mocked_path, 42, @scope, 'inspect', []) expect(result).to eq("Scope(<module>/mymodule/oz.pp, 42)") end it "should return a scope for use in a test harness" do expect(create_test_scope_for_node("node_name_foo")).to be_a_kind_of(Puppet::Parser::Scope) end it "should be able to retrieve class scopes by name" do @scope.class_set "myname", "myscope" expect(@scope.class_scope("myname")).to eq("myscope") end it "should be able to retrieve class scopes by object" do klass = double('ast_class') expect(klass).to receive(:name).and_return("myname") @scope.class_set "myname", "myscope" expect(@scope.class_scope(klass)).to eq("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") expect(@scope.parent_module_name).to eq("foo") end it "should return a nil parent module name if it has no parent" do expect(@topscope.parent_module_name).to be_nil end it "should return a nil parent module name if its parent has no source" do expect(@scope.parent_module_name).to be_nil end it "should get its environment from its compiler" do env = Puppet::Node::Environment.create(:testing, []) compiler = double('compiler', :environment => env, :is_a? => true) scope = Puppet::Parser::Scope.new(compiler) expect(scope.environment).to equal(env) end it "should fail if no compiler is supplied" do expect { Puppet::Parser::Scope.new }.to raise_error(ArgumentError, /wrong number of arguments/) end it "should fail if something that isn't a compiler is supplied" do expect { Puppet::Parser::Scope.new(nil) }.to raise_error(Puppet::DevError, /you must pass a compiler instance/) end describe "when custom functions are called" do let(:env) { Puppet::Node::Environment.create(:testing, []) } let(:compiler) { Puppet::Parser::Compiler.new(Puppet::Node.new('foo', :environment => env)) } let(:scope) { Puppet::Parser::Scope.new(compiler) } it "calls methods prefixed with function_ as custom functions" do expect(scope.function_sprintf(["%b", 123])).to eq("1111011") end it "raises an error when arguments are not passed in an Array" do expect do scope.function_sprintf("%b", 123) end.to raise_error ArgumentError, /custom functions must be called with a single array that contains the arguments/ end it "raises an error on subsequent calls when arguments are not passed in an Array" do scope.function_sprintf(["first call"]) expect do scope.function_sprintf("%b", 123) end.to raise_error ArgumentError, /custom functions must be called with a single array that contains the arguments/ end it "raises NoMethodError when the not prefixed" do expect { scope.sprintf(["%b", 123]) }.to raise_error(NoMethodError) end it "raises NoMethodError when prefixed with function_ but it doesn't exist" do expect { scope.function_fake_bs(['cows']) }.to 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.create(:myenv, []) root = Puppet.lookup(:root_environment) compiler = double('compiler', :environment => env, :is_a? => true) scope = Puppet::Parser::Scope.new(compiler) expect(scope.singleton_class.ancestors).to be_include(Puppet::Parser::Functions.environment_module(env)) expect(scope.singleton_class.ancestors).to 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.lookup(:root_environment) node = Puppet::Node.new('localhost') compiler = Puppet::Parser::Compiler.new(node) scope = Puppet::Parser::Scope.new(compiler) expect(scope.singleton_class.ancestors).to be_include(Puppet::Parser::Functions.environment_module(root)) end end describe "when looking up a variable" do before :each do Puppet[:strict] = :warning end it "should support :lookupvar and :setvar for backward compatibility" do @scope.setvar("var", "yep") expect(@scope.lookupvar("var")).to eq("yep") end it "should fail if invoked with a non-string name" do expect { @scope[:foo] }.to raise_error(Puppet::ParseError, /Scope variable name .* not a string/) expect { @scope[:foo] = 12 }.to raise_error(Puppet::ParseError, /Scope variable name .* not a string/) end it "should return nil for unset variables when --strict variables is not in effect" do expect(@scope["var"]).to be_nil end it "answers exist? with boolean false for non existing variables" do expect(@scope.exist?("var")).to be(false) end it "answers exist? with boolean false for non existing variables" do @scope["var"] = "yep" expect(@scope.exist?("var")).to be(true) end it "should be able to look up values" do @scope["var"] = "yep" expect(@scope["var"]).to eq("yep") end it "should be able to look up hashes" do @scope["var"] = {"a" => "b"} expect(@scope["var"]).to eq({"a" => "b"}) end it "should be able to look up variables in parent scopes" do @topscope["var"] = "parentval" expect(@scope["var"]).to eq("parentval") end it "should prefer its own values to parent values" do @topscope["var"] = "parentval" @scope["var"] = "childval" expect(@scope["var"]).to eq("childval") end it "should be able to detect when variables are set" do @scope["var"] = "childval" expect(@scope).to be_include("var") end it "does not allow changing a set value" do @scope["var"] = "childval" expect { @scope["var"] = "change" }.to raise_error(Puppet::Error, "Cannot reassign variable '$var'") end it "should be able to detect when variables are not set" do expect(@scope).not_to be_include("var") end it "warns and return nil for non found unqualified variable" do expect(Puppet).to receive(:warn_once) expect(@scope["santa_clause"]).to be_nil end it "warns once for a non found variable" do expect(Puppet).to receive(:send_log).with(:warning, be_a(String)).once expect([@scope["santa_claus"],@scope["santa_claus"]]).to eq([nil, nil]) end it "warns and return nil for non found qualified variable" do expect(Puppet).to receive(:warn_once) expect(@scope["north_pole::santa_clause"]).to be_nil end it "does not warn when a numeric variable is missing - they always exist" do expect(Puppet).not_to receive(:warn_once) expect(@scope["1"]).to be_nil end describe "and the variable is qualified" do before :each do @known_resource_types = @scope.environment.known_resource_types node = Puppet::Node.new('localhost') @compiler = Puppet::Parser::Compiler.new(node) 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(@compiler))) Puppet::Parser::Resource.new("class", name, :scope => @scope, :source => double('source'), :catalog => catalog).evaluate @scope.class_scope(klass) end it "should be able to look up explicitly fully qualified variables from compiler's top scope" do expect(Puppet).not_to receive(:deprecation_warning) other_scope = @scope.compiler.topscope other_scope["othervar"] = "otherval" expect(@scope["::othervar"]).to eq("otherval") end it "should be able to look up explicitly fully qualified variables from other scopes" do expect(Puppet).not_to receive(:deprecation_warning) other_scope = create_class_scope("other") other_scope["var"] = "otherval" expect(@scope["::other::var"]).to eq("otherval") end it "should be able to look up deeply qualified variables" do expect(Puppet).not_to receive(:deprecation_warning) other_scope = create_class_scope("other::deep::klass") other_scope["var"] = "otherval" expect(@scope["other::deep::klass::var"]).to eq("otherval") end it "should return nil for qualified variables that cannot be found in other classes" do create_class_scope("other::deep::klass") expect(@scope["other::deep::klass::var"]).to be_nil end it "should warn and return nil for qualified variables whose classes have not been evaluated" do newclass("other::deep::klass") expect(Puppet).to receive(:warn_once) expect(@scope["other::deep::klass::var"]).to be_nil end it "should warn and return nil for qualified variables whose classes do not exist" do expect(Puppet).to receive(:warn_once) expect(@scope["other::deep::klass::var"]).to be_nil end it "should return nil when asked for a non-string qualified variable from a class that does not exist" do expect(@scope["other::deep::klass::var"]).to be_nil end it "should return nil when asked for a non-string qualified variable from a class that has not been evaluated" do allow(@scope).to receive(:warning) newclass("other::deep::klass") expect(@scope["other::deep::klass::var"]).to be_nil end end context "and strict_variables is true" do before(:each) do Puppet[:strict_variables] = true end it "should throw a symbol when unknown variable is looked up" do expect { @scope['john_doe'] }.to throw_symbol(:undefined_variable) end it "should throw a symbol when unknown qualified variable is looked up" do expect { @scope['nowhere::john_doe'] }.to throw_symbol(:undefined_variable) end it "should not raise an error when built in variable is looked up" do expect { @scope['caller_module_name'] }.to_not raise_error expect { @scope['module_name'] }.to_not raise_error end end context "and strict_variables is false and --strict=off" do before(:each) do Puppet[:strict_variables] = false Puppet[:strict] = :off end it "should not error when unknown variable is looked up and produce nil" do expect(@scope['john_doe']).to be_nil end it "should not error when unknown qualified variable is looked up and produce nil" do expect(@scope['nowhere::john_doe']).to be_nil end end context "and strict_variables is false and --strict=warning" do before(:each) do Puppet[:strict_variables] = false Puppet[:strict] = :warning end it "should not error when unknown variable is looked up" do expect(@scope['john_doe']).to be_nil end it "should not error when unknown qualified variable is looked up" do expect(@scope['nowhere::john_doe']).to be_nil end end context "and strict_variables is false and --strict=error" do before(:each) do Puppet[:strict_variables] = false Puppet[:strict] = :error end it "should raise error when unknown variable is looked up" do expect { @scope['john_doe'] }.to raise_error(/Undefined variable/) end it "should not throw a symbol when unknown qualified variable is looked up" do expect { @scope['nowhere::john_doe'] }.to raise_error(/Undefined variable/) end end end describe "when calling number?" do it "should return nil if called with anything not a number" do expect(Puppet::Parser::Scope.number?([2])).to be_nil end it "should return a Integer for an Integer" do expect(Puppet::Parser::Scope.number?(2)).to be_a(Integer) end it "should return a Float for a Float" do expect(Puppet::Parser::Scope.number?(2.34)).to be_an_instance_of(Float) end it "should return 234 for '234'" do expect(Puppet::Parser::Scope.number?("234")).to eq(234) end it "should return nil for 'not a number'" do expect(Puppet::Parser::Scope.number?("not a number")).to be_nil end it "should return 23.4 for '23.4'" do expect(Puppet::Parser::Scope.number?("23.4")).to eq(23.4) end it "should return 23.4e13 for '23.4e13'" do expect(Puppet::Parser::Scope.number?("23.4e13")).to eq(23.4e13) end it "should understand negative numbers" do expect(Puppet::Parser::Scope.number?("-234")).to eq(-234) end it "should know how to convert exponential float numbers ala '23e13'" do expect(Puppet::Parser::Scope.number?("23e13")).to eq(23e13) end it "should understand hexadecimal numbers" do expect(Puppet::Parser::Scope.number?("0x234")).to eq(0x234) end it "should understand octal numbers" do expect(Puppet::Parser::Scope.number?("0755")).to eq(0755) end it "should return nil on malformed integers" do expect(Puppet::Parser::Scope.number?("0.24.5")).to be_nil end it "should convert strings with leading 0 to integer if they are not octal" do expect(Puppet::Parser::Scope.number?("0788")).to eq(788) end it "should convert strings of negative integers" do expect(Puppet::Parser::Scope.number?("-0788")).to eq(-788) end it "should return nil on malformed hexadecimal numbers" do expect(Puppet::Parser::Scope.number?("0x89g")).to be_nil end end describe "when using ephemeral variables" do it "should store the variable value" do @scope.set_match_data({1 => :value}) expect(@scope["1"]).to eq(:value) end it "should raise an error when setting numerical variable" do expect { @scope.setvar("1", :value3, :ephemeral => true) }.to raise_error(Puppet::ParseError, /Cannot assign to a numeric match result variable/) end describe "with more than one level" do it "should prefer latest ephemeral scopes" do @scope.set_match_data({0 => :earliest}) @scope.new_ephemeral @scope.set_match_data({0 => :latest}) expect(@scope["0"]).to eq(:latest) end it "should be able to report the current level" do expect(@scope.ephemeral_level).to eq(1) @scope.new_ephemeral expect(@scope.ephemeral_level).to eq(2) end it "should not check presence of an ephemeral variable across multiple levels" do @scope.new_ephemeral @scope.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({0 => :value2}) @scope.new_ephemeral expect(@scope.include?("1")).to be_falsey end it "should return false when an ephemeral variable doesn't exist in any ephemeral scope" do @scope.new_ephemeral @scope.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({0 => :value2}) @scope.new_ephemeral expect(@scope.include?("2")).to be_falsey end it "should not get ephemeral values from earlier scope when not in later" do @scope.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({0 => :value2}) expect(@scope.include?("1")).to be_falsey end describe "when using a guarded scope" do it "should remove ephemeral scopes up to this level" do @scope.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({1 => :value2}) @scope.with_guarded_scope do @scope.new_ephemeral @scope.set_match_data({1 => :value3}) end expect(@scope["1"]).to eq(:value2) end end end end context "when using ephemeral as local scope" do it "should store all variables in local scope" do @scope.new_ephemeral true @scope.setvar("apple", :fruit) expect(@scope["apple"]).to eq(:fruit) end it 'should store an undef in local scope and let it override parent scope' do @scope['cloaked'] = 'Cloak me please' @scope.new_ephemeral(true) @scope['cloaked'] = nil expect(@scope['cloaked']).to eq(nil) end it "should be created from a hash" do @scope.ephemeral_from({ "apple" => :fruit, "strawberry" => :berry}) expect(@scope["apple"]).to eq(:fruit) expect(@scope["strawberry"]).to eq(:berry) end end describe "when setting ephemeral vars from matches" do before :each do @match = double('match', :is_a? => true) allow(@match).to receive(:[]).with(0).and_return("this is a string") allow(@match).to receive(:captures).and_return([]) allow(@scope).to receive(:setvar) end it "should accept only MatchData" do expect { @scope.ephemeral_from("match") }.to raise_error(ArgumentError, /Invalid regex match data/) end it "should set $0 with the full match" do # This is an internal impl detail test expect(@scope).to receive(:new_match_scope) do |arg| expect(arg[0]).to eq("this is a string") end @scope.ephemeral_from(@match) end it "should set every capture as ephemeral var" do # This is an internal impl detail test allow(@match).to receive(:[]).with(1).and_return(:capture1) allow(@match).to receive(:[]).with(2).and_return(:capture2) expect(@scope).to receive(:new_match_scope) do |arg| expect(arg[1]).to eq(:capture1) expect(arg[2]).to eq(:capture2) end @scope.ephemeral_from(@match) end it "should shadow previous match variables" do # This is an internal impl detail test allow(@match).to receive(:[]).with(1).and_return(:capture1) allow(@match).to receive(:[]).with(2).and_return(:capture2) @match2 = double('match', :is_a? => true) allow(@match2).to receive(:[]).with(1).and_return(:capture2_1) allow(@match2).to receive(:[]).with(2).and_return(nil) @scope.ephemeral_from(@match) @scope.ephemeral_from(@match2) expect(@scope.lookupvar('2')).to eq(nil) end it "should create a new ephemeral level" do level_before = @scope.ephemeral_level @scope.ephemeral_from(@match) expect(level_before < @scope.ephemeral_level) end 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 => double("source")) @scope.define_settings(:mytype, param) expect(@scope.lookupdefaults(:mytype)).to eq({: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 => double("source")) @scope.define_settings(:mytype, param) expect { @scope.define_settings(:mytype, param) }.to raise_error(Puppet::ParseError, /Default already defined .* cannot redefine/) end it "should return multiple defaults at once" do param1 = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => double("source")) @scope.define_settings(:mytype, param1) param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => double("source")) @scope.define_settings(:mytype, param2) expect(@scope.lookupdefaults(:mytype)).to eq({: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 => double("source")) @scope.define_settings(:mytype, param1) child_scope = @scope.newscope param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => double("source")) child_scope.define_settings(:mytype, param2) expect(child_scope.lookupdefaults(:mytype)).to eq({:myparam => param1, :other => param2}) end end context "#true?" do { "a string" => true, "true" => true, "false" => true, true => true, "" => false, :undef => false, nil => false }.each do |input, output| it "should treat #{input.inspect} as #{output}" do expect(Puppet::Parser::Scope.true?(input)).to eq(output) end end end context "when producing a hash of all variables (as used in templates)" do it "should contain all defined variables in the scope" do @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) expect(@scope.to_hash).to eq({'orange' => :tangerine, 'pear' => :green }) end it "should contain variables in all local scopes (#21508)" do @scope.new_ephemeral true @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) @scope.new_ephemeral true @scope.setvar("apple", :red) expect(@scope.to_hash).to eq({'orange' => :tangerine, 'pear' => :green, 'apple' => :red }) end it "should contain all defined variables in the scope and all local scopes" do @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) @scope.new_ephemeral true @scope.setvar("apple", :red) expect(@scope.to_hash).to eq({'orange' => :tangerine, 'pear' => :green, 'apple' => :red }) end it "should not contain varaibles in match scopes (non local emphemeral)" do @scope.new_ephemeral true @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) @scope.ephemeral_from(/(f)(o)(o)/.match('foo')) expect(@scope.to_hash).to eq({'orange' => :tangerine, 'pear' => :green }) end it "should delete values that are :undef in inner scope" do @scope.new_ephemeral true @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) @scope.new_ephemeral true @scope.setvar("apple", :red) @scope.setvar("orange", :undef) expect(@scope.to_hash).to eq({'pear' => :green, 'apple' => :red }) end it "should not delete values that are :undef in inner scope when include_undef is true" do @scope.new_ephemeral true @scope.setvar("orange", :tangerine) @scope.setvar("pear", :green) @scope.new_ephemeral true @scope.setvar("apple", :red) @scope.setvar("orange", :undef) expect(@scope.to_hash(true, true)).to eq({'pear' => :green, 'apple' => :red, 'orange' => :undef }) end end end