diff --git a/lib/puppet/parser/ast/function.rb b/lib/puppet/parser/ast/function.rb index 602016c75..74023f631 100644 --- a/lib/puppet/parser/ast/function.rb +++ b/lib/puppet/parser/ast/function.rb @@ -1,51 +1,50 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # An AST object to call a function. class Function < AST::Branch associates_doc attr_accessor :name, :arguments @settor = true def evaluate(scope) - # Make sure it's a defined function raise Puppet::ParseError, "Unknown function #{@name}" unless Puppet::Parser::Functions.function(@name) # Now check that it's been used correctly case @ftype when :rvalue raise Puppet::ParseError, "Function '#{@name}' does not return a value" unless Puppet::Parser::Functions.rvalue?(@name) when :statement if Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '#{@name}' must be the value of a statement" end else raise Puppet::DevError, "Invalid function type #{@ftype.inspect}" end # We don't need to evaluate the name, because it's plaintext args = @arguments.safeevaluate(scope) scope.send("function_#{@name}", args) end def initialize(hash) @ftype = hash[:ftype] || :rvalue hash.delete(:ftype) if hash.include? :ftype super(hash) # Lastly, check the parity end def to_s args = arguments.is_a?(ASTArray) ? arguments.to_s.gsub(/\[(.*)\]/,'\1') : arguments "#{name}(#{args})" end end end diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index 49cde63ca..090d75c4e 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,213 +1,213 @@ class Puppet::Parser::AST # The base class for all of the leaves of the parse trees. These # basically just have types and values. Both of these parameters # are simple values, not AST objects. class Leaf < AST attr_accessor :value, :type # Return our value. def evaluate(scope) @value end def match(value) @value == value end def to_s @value.to_s unless @value.nil? end end # The boolean class. True or false. Converts the string it receives # to a Ruby boolean. class Boolean < AST::Leaf # Use the parent method, but then convert to a real boolean. def initialize(hash) super unless @value == true or @value == false raise Puppet::DevError, "'#{@value}' is not a boolean" end @value end def to_s @value ? "true" : "false" end end # The base string class. class String < AST::Leaf def evaluate(scope) - @value + @value.dup end def to_s "\"#{@value}\"" end end # An uninterpreted string. class FlatString < AST::Leaf def evaluate(scope) @value end def to_s "\"#{@value}\"" end end class Concat < AST::Leaf def evaluate(scope) @value.collect { |x| x.evaluate(scope) }.collect{ |x| x == :undef ? '' : x }.join end def to_s "concat(#{@value.join(',')})" end end # The 'default' option on case statements and selectors. class Default < AST::Leaf; end # Capitalized words; used mostly for type-defaults, but also # get returned by the lexer any other time an unquoted capitalized # word is found. class Type < AST::Leaf; end # Lower-case words. class Name < AST::Leaf; end # double-colon separated class names class ClassName < AST::Leaf; end # undef values; equiv to nil class Undef < AST::Leaf; end # Host names, either fully qualified or just the short name, or even a regex class HostName < AST::Leaf def initialize(hash) super # Note that this is an AST::Regex, not a Regexp @value = @value.to_s.downcase unless @value.is_a?(Regex) if @value =~ /[^-\w.]/ raise Puppet::DevError, "'#{@value}' is not a valid hostname" end end # implementing eql? and hash so that when an HostName is stored # in a hash it has the same hashing properties as the underlying value def eql?(value) value = value.value if value.is_a?(HostName) @value.eql?(value) end def hash @value.hash end def to_s @value.to_s end end # A simple variable. This object is only used during interpolation; # the VarDef class is used for assignment. class Variable < Name # Looks up the value of the object in the scope tree (does # not include syntactical constructs, like '$' and '{}'). def evaluate(scope) parsewrap do if (var = scope.lookupvar(@value, false)) == :undefined var = :undef end var end end def to_s "\$#{value}" end end class HashOrArrayAccess < AST::Leaf attr_accessor :variable, :key def evaluate_container(scope) container = variable.respond_to?(:evaluate) ? variable.safeevaluate(scope) : variable (container.is_a?(Hash) or container.is_a?(Array)) ? container : scope.lookupvar(container) end def evaluate_key(scope) key.respond_to?(:evaluate) ? key.safeevaluate(scope) : key end def evaluate(scope) object = evaluate_container(scope) raise Puppet::ParseError, "#{variable} is not an hash or array when accessing it with #{accesskey}" unless object.is_a?(Hash) or object.is_a?(Array) object[evaluate_key(scope)] end # Assign value to this hashkey or array index def assign(scope, value) object = evaluate_container(scope) accesskey = evaluate_key(scope) if object.is_a?(Hash) and object.include?(accesskey) raise Puppet::ParseError, "Assigning to the hash '#{variable}' with an existing key '#{accesskey}' is forbidden" end # assign to hash or array object[accesskey] = value end def to_s "\$#{variable.to_s}[#{key.to_s}]" end end class Regex < AST::Leaf def initialize(hash) super @value = Regexp.new(@value) unless @value.is_a?(Regexp) end # we're returning self here to wrap the regexp and to be used in places # where a string would have been used, without modifying any client code. # For instance, in many places we have the following code snippet: # val = @val.safeevaluate(@scope) # if val.match(otherval) # ... # end # this way, we don't have to modify this test specifically for handling # regexes. def evaluate(scope) self end def evaluate_match(value, scope, options = {}) value = value.is_a?(String) ? value : value.to_s if matched = @value.match(value) scope.ephemeral_from(matched, options[:file], options[:line]) end matched end def match(value) @value.match(value) end def to_s "/#{@value.source}/" end end end diff --git a/lib/puppet/parser/functions/extlookup.rb b/lib/puppet/parser/functions/extlookup.rb index ee230e7ce..177d9aef8 100644 --- a/lib/puppet/parser/functions/extlookup.rb +++ b/lib/puppet/parser/functions/extlookup.rb @@ -1,173 +1,169 @@ # Puppet External Data Sources # # This is a parser function to read data from external files, this version # uses CSV files but the concept can easily be adjust for databases, yaml # or any other queryable data source. # # The object of this is to make it obvious when it's being used, rather than # magically loading data in when an module is loaded I prefer to look at the code # and see statements like: # # $snmp_contact = extlookup("snmp_contact") # # The above snippet will load the snmp_contact value from CSV files, this in its # own is useful but a common construct in puppet manifests is something like this: # # case $domain { # "myclient.com": { $snmp_contact = "John Doe " } # default: { $snmp_contact = "My Support " } # } # # Over time there will be a lot of this kind of thing spread all over your manifests # and adding an additional client involves grepping through manifests to find all the # places where you have constructs like this. # # This is a data problem and shouldn't be handled in code, a using this function you # can do just that. # # First you configure it in site.pp: # $extlookup_datadir = "/etc/puppet/manifests/extdata" # $extlookup_precedence = ["%{fqdn}", "domain_%{domain}", "common"] # # The array tells the code how to resolve values, first it will try to find it in # web1.myclient.com.csv then in domain_myclient.com.csv and finally in common.csv # # Now create the following data files in /etc/puppet/manifests/extdata # # domain_myclient.com.csv: # snmp_contact,John Doe # root_contact,support@%{domain} # client_trusted_ips,192.168.1.130,192.168.10.0/24 # # common.csv: # snmp_contact,My Support # root_contact,support@my.com # # Now you can replace the case statement with the simple single line to achieve # the exact same outcome: # # $snmp_contact = extlookup("snmp_contact") # # The obove code shows some other features, you can use any fact or variable that # is in scope by simply using %{varname} in your data files, you can return arrays # by just having multiple values in the csv after the initial variable name. # # In the event that a variable is nowhere to be found a critical error will be raised # that will prevent your manifest from compiling, this is to avoid accidentally putting # in empty values etc. You can however specify a default value: # # $ntp_servers = extlookup("ntp_servers", "1.${country}.pool.ntp.org") # # In this case it will default to "1.${country}.pool.ntp.org" if nothing is defined in # any data file. # # You can also specify an additional data file to search first before any others at use # time, for example: # # $version = extlookup("rsyslog_version", "present", "packages") # # package{"rsyslog": ensure => $version } # # This will look for a version configured in packages.csv and then in the rest as configured # by $extlookup_precedence if it's not found anywhere it will default to "present", this kind # of use case makes puppet a lot nicer for managing large amounts of packages since you do not # need to edit a load of manifests to do simple things like adjust a desired version number. # # For more information on installing and writing your own custom functions see: # http://docs.puppetlabs.com/guides/custom_functions.html # # For further help contact Volcane on #puppet require 'csv' module Puppet::Parser::Functions newfunction(:extlookup, :type => :rvalue) do |args| key = args[0] default = args[1] datafile = args[2] raise Puppet::ParseError, ("extlookup(): wrong number of arguments (#{args.length}; must be <= 3)") if args.length > 3 extlookup_datadir = lookupvar('extlookup_datadir') extlookup_precedence = Array.new # precedence values can have variables embedded in them # in the form %{fqdn}, you could for example do # # $extlookup_precedence = ["hosts/%{fqdn}", "common"] # # this will result in /path/to/extdata/hosts/your.box.com.csv # being searched. # - # we parse the precedence here because the best place to specify - # it would be in site.pp but site.pp is only evaluated at startup - # so $fqdn etc would have no meaning there, this way it gets evaluated - # each run and has access to the right variables for that run - lookupvar('extlookup_precedence').each do |prec| - while prec =~ /%\{(.+?)\}/ - prec.gsub!(/%\{#{$1}\}/, lookupvar($1)) + # This is for back compatibility to interpolate variables with %. + # % interpolation is a workaround for a problem that has been fixed: Puppet variable + # interpolation at top scope used to only happen on each run + extlookup_precedence = lookupvar('extlookup_precedence').collect do |var| + var.gsub(/%\{(.+?)\}/) do |capture| + lookupvar($1) end - - extlookup_precedence << prec end - datafiles = Array.new # if we got a custom data file, put it first in the array of search files if datafile != "" datafiles << extlookup_datadir + "/#{datafile}.csv" if File.exists?(extlookup_datadir + "/#{datafile}.csv") end extlookup_precedence.each do |d| datafiles << extlookup_datadir + "/#{d}.csv" end desired = nil datafiles.each do |file| parser = Puppet::Parser::Parser.new(environment) parser.watch_file(file) if File.exists?(file) if desired.nil? if File.exists?(file) result = CSV.read(file).find_all do |r| r[0] == key end # return just the single result if theres just one, # else take all the fields in the csv and build an array if result.length > 0 if result[0].length == 2 val = result[0][1].to_s # parse %{}'s in the CSV into local variables using lookupvar() while val =~ /%\{(.+?)\}/ val.gsub!(/%\{#{$1}\}/, lookupvar($1)) end desired = val elsif result[0].length > 1 length = result[0].length cells = result[0][1,length] # Individual cells in a CSV result are a weird data type and throws # puppets yaml parsing, so just map it all to plain old strings desired = cells.map do |c| # parse %{}'s in the CSV into local variables using lookupvar() while c =~ /%\{(.+?)\}/ c.gsub!(/%\{#{$1}\}/, lookupvar($1)) end c.to_s end end end end end end desired || default or raise Puppet::ParseError, "No match found for '#{key}' in any data file during extlookup()" end end diff --git a/spec/unit/parser/ast/leaf_spec.rb b/spec/unit/parser/ast/leaf_spec.rb index 5bdca67fa..eb71f0d37 100755 --- a/spec/unit/parser/ast/leaf_spec.rb +++ b/spec/unit/parser/ast/leaf_spec.rb @@ -1,362 +1,366 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::Leaf do before :each do @scope = stub 'scope' @value = stub 'value' @leaf = Puppet::Parser::AST::Leaf.new(:value => @value) end it "should have a evaluate_match method" do Puppet::Parser::AST::Leaf.new(:value => "value").should respond_to(:evaluate_match) end describe "when converting to string" do it "should transform its value to string" do value = stub 'value', :is_a? => true value.expects(:to_s) Puppet::Parser::AST::Leaf.new( :value => value ).to_s end end it "should have a match method" do @leaf.should respond_to(:match) end it "should delegate match to ==" do @value.expects(:==).with("value") @leaf.match("value") end end describe Puppet::Parser::AST::FlatString do describe "when converting to string" do it "should transform its value to a quoted string" do value = stub 'value', :is_a? => true, :to_s => "ab" Puppet::Parser::AST::FlatString.new( :value => value ).to_s.should == "\"ab\"" end end end describe Puppet::Parser::AST::String do describe "when converting to string" do it "should transform its value to a quoted string" do value = stub 'value', :is_a? => true, :to_s => "ab" Puppet::Parser::AST::String.new( :value => value ).to_s.should == "\"ab\"" end + it "should return a dup of its value" do + value = "" + Puppet::Parser::AST::String.new( :value => value ).evaluate(stub 'scope').should_not be_equal(value) + end end end describe Puppet::Parser::AST::Concat do describe "when evaluating" do before :each do @scope = stub_everything 'scope' end it "should interpolate variables and concatenate their values" do one = Puppet::Parser::AST::String.new(:value => "one") one.stubs(:evaluate).returns("one ") two = Puppet::Parser::AST::String.new(:value => "two") two.stubs(:evaluate).returns(" two ") three = Puppet::Parser::AST::String.new(:value => "three") three.stubs(:evaluate).returns(" three") var = Puppet::Parser::AST::Variable.new(:value => "myvar") var.stubs(:evaluate).returns("foo") array = Puppet::Parser::AST::Variable.new(:value => "array") array.stubs(:evaluate).returns(["bar","baz"]) concat = Puppet::Parser::AST::Concat.new(:value => [one,var,two,array,three]) concat.evaluate(@scope).should == 'one foo two barbaz three' end it "should transform undef variables to empty string" do var = Puppet::Parser::AST::Variable.new(:value => "myvar") var.stubs(:evaluate).returns(:undef) concat = Puppet::Parser::AST::Concat.new(:value => [var]) concat.evaluate(@scope).should == '' end end end describe Puppet::Parser::AST::Undef do before :each do @scope = stub 'scope' @undef = Puppet::Parser::AST::Undef.new(:value => :undef) end it "should match undef with undef" do @undef.evaluate_match(:undef, @scope).should be_true end it "should not match undef with an empty string" do @undef.evaluate_match("", @scope).should be_false end end describe Puppet::Parser::AST::HashOrArrayAccess do before :each do @scope = stub 'scope' end describe "when evaluating" do it "should evaluate the variable part if necessary" do @scope.stubs(:lookupvar).with("a").returns(["b"]) variable = stub 'variable', :evaluate => "a" access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => variable, :key => 0 ) variable.expects(:safeevaluate).with(@scope).returns("a") access.evaluate(@scope).should == "b" end it "should evaluate the access key part if necessary" do @scope.stubs(:lookupvar).with("a").returns(["b"]) index = stub 'index', :evaluate => 0 access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => index ) index.expects(:safeevaluate).with(@scope).returns(0) access.evaluate(@scope).should == "b" end it "should be able to return an array member" do @scope.stubs(:lookupvar).with("a").returns(["val1", "val2", "val3"]) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 1 ) access.evaluate(@scope).should == "val2" end it "should be able to return an hash value" do @scope.stubs(:lookupvar).with("a").returns({ "key1" => "val1", "key2" => "val2", "key3" => "val3" }) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) access.evaluate(@scope).should == "val2" end it "should raise an error if the variable lookup didn't return an hash or an array" do @scope.stubs(:lookupvar).with("a").returns("I'm a string") access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) lambda { access.evaluate(@scope) }.should raise_error end it "should raise an error if the variable wasn't in the scope" do @scope.stubs(:lookupvar).with("a").returns(nil) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) lambda { access.evaluate(@scope) }.should raise_error end it "should return a correct string representation" do access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) access.to_s.should == '$a[key2]' end it "should work with recursive hash access" do @scope.stubs(:lookupvar).with("a").returns({ "key" => { "subkey" => "b" }}) access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => "subkey") access2.evaluate(@scope).should == 'b' end it "should work with interleaved array and hash access" do @scope.stubs(:lookupvar).with("a").returns({ "key" => [ "a" , "b" ]}) access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => 1) access2.evaluate(@scope).should == 'b' end end describe "when assigning" do it "should add a new key and value" do scope = Puppet::Parser::Scope.new scope.setvar("a", { 'a' => 'b' }) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "b") access.assign(scope, "c" ) scope.lookupvar("a").should be_include("b") end it "should raise an error when trying to overwrite an hash value" do @scope.stubs(:lookupvar).with("a").returns({ "key" => [ "a" , "b" ]}) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") lambda { access.assign(@scope, "test") }.should raise_error end end end describe Puppet::Parser::AST::Regex do before :each do @scope = stub 'scope' end describe "when initializing" do it "should create a Regexp with its content when value is not a Regexp" do Regexp.expects(:new).with("/ab/") Puppet::Parser::AST::Regex.new :value => "/ab/" end it "should not create a Regexp with its content when value is a Regexp" do value = Regexp.new("/ab/") Regexp.expects(:new).with("/ab/").never Puppet::Parser::AST::Regex.new :value => value end end describe "when evaluating" do it "should return self" do val = Puppet::Parser::AST::Regex.new :value => "/ab/" val.evaluate(@scope).should === val end end describe "when evaluate_match" do before :each do @value = stub 'regex' @value.stubs(:match).with("value").returns(true) Regexp.stubs(:new).returns(@value) @regex = Puppet::Parser::AST::Regex.new :value => "/ab/" end it "should issue the regexp match" do @value.expects(:match).with("value") @regex.evaluate_match("value", @scope) end it "should not downcase the paramater value" do @value.expects(:match).with("VaLuE") @regex.evaluate_match("VaLuE", @scope) end it "should set ephemeral scope vars if there is a match" do @scope.expects(:ephemeral_from).with(true, nil, nil) @regex.evaluate_match("value", @scope) end it "should return the match to the caller" do @value.stubs(:match).with("value").returns(:match) @scope.stubs(:ephemeral_from) @regex.evaluate_match("value", @scope) end end it "should return the regex source with to_s" do regex = stub 'regex' Regexp.stubs(:new).returns(regex) val = Puppet::Parser::AST::Regex.new :value => "/ab/" regex.expects(:source) val.to_s end it "should delegate match to the underlying regexp match method" do regex = Regexp.new("/ab/") val = Puppet::Parser::AST::Regex.new :value => regex regex.expects(:match).with("value") val.match("value") end end describe Puppet::Parser::AST::Variable do before :each do @scope = stub 'scope' @var = Puppet::Parser::AST::Variable.new(:value => "myvar") end it "should lookup the variable in scope" do @scope.expects(:lookupvar).with("myvar", false).returns(:myvalue) @var.safeevaluate(@scope).should == :myvalue end it "should return undef if the variable wasn't set" do @scope.expects(:lookupvar).with("myvar", false).returns(:undefined) @var.safeevaluate(@scope).should == :undef end describe "when converting to string" do it "should transform its value to a variable" do value = stub 'value', :is_a? => true, :to_s => "myvar" Puppet::Parser::AST::Variable.new( :value => value ).to_s.should == "\$myvar" end end end describe Puppet::Parser::AST::HostName do before :each do @scope = stub 'scope' @value = stub 'value', :=~ => false @value.stubs(:to_s).returns(@value) @value.stubs(:downcase).returns(@value) @host = Puppet::Parser::AST::HostName.new( :value => @value) end it "should raise an error if hostname is not valid" do lambda { Puppet::Parser::AST::HostName.new( :value => "not an hostname!" ) }.should raise_error end it "should not raise an error if hostname is a regex" do lambda { Puppet::Parser::AST::HostName.new( :value => Puppet::Parser::AST::Regex.new(:value => "/test/") ) }.should_not raise_error end it "should stringify the value" do value = stub 'value', :=~ => false value.expects(:to_s).returns("test") Puppet::Parser::AST::HostName.new(:value => value) end it "should downcase the value" do value = stub 'value', :=~ => false value.stubs(:to_s).returns("UPCASED") host = Puppet::Parser::AST::HostName.new(:value => value) host.value == "upcased" end it "should evaluate to its value" do @host.evaluate(@scope).should == @value end it "should delegate eql? to the underlying value if it is an HostName" do @value.expects(:eql?).with("value") @host.eql?("value") end it "should delegate eql? to the underlying value if it is not an HostName" do value = stub 'compared', :is_a? => true, :value => "value" @value.expects(:eql?).with("value") @host.eql?(value) end it "should delegate hash to the underlying value" do @value.expects(:hash) @host.hash end end diff --git a/spec/unit/parser/functions/extlookup_spec.rb b/spec/unit/parser/functions/extlookup_spec.rb index bf2880345..a3dcaa742 100755 --- a/spec/unit/parser/functions/extlookup_spec.rb +++ b/spec/unit/parser/functions/extlookup_spec.rb @@ -1,85 +1,95 @@ #! /usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'tempfile' describe "the extlookup function" do before :each do @scope = Puppet::Parser::Scope.new - + @scope.stubs(:environment).returns(Puppet::Node::Environment.new('production')) + Puppet::Parser::Functions.function("extlookup") end it "should exist" do Puppet::Parser::Functions.function("extlookup").should == "function_extlookup" end it "should raise a ParseError if there is less than 1 arguments" do lambda { @scope.function_extlookup([]) }.should( raise_error(Puppet::ParseError)) end it "should raise a ParseError if there is more than 3 arguments" do lambda { @scope.function_extlookup(["foo", "bar", "baz", "gazonk"]) }.should( raise_error(Puppet::ParseError)) end it "should return the default" do result = @scope.function_extlookup([ "key", "default"]) result.should == "default" end it "should lookup the key in a supplied datafile" do t = Tempfile.new('extlookup.csv') do t.puts 'key,value' t.puts 'nonkey,nonvalue' t.close result = @scope.function_extlookup([ "key", "default", t.path]) result.should == "value" end end it "should return an array if the datafile contains more than two columns" do t = Tempfile.new('extlookup.csv') do t.puts 'key,value1,value2' t.puts 'nonkey,nonvalue,nonvalue' t.close result = @scope.function_extlookup([ "key", "default", t.path]) result.should == ["value1", "value2"] end end it "should raise an error if there's no matching key and no default" do t = Tempfile.new('extlookup.csv') do t.puts 'key,value' t.puts 'nonkey,nonvalue' t.close result = @scope.function_extlookup([ "key", nil, t.path]) result.should == "value" end end describe "should look in $extlookup_datadir for data files listed by $extlookup_precedence" do - before do + before do @scope.stubs(:lookupvar).with('extlookup_datadir').returns("/tmp") - @scope.stubs(:lookupvar).with('extlookup_precedence').returns(["one","two"]) File.open("/tmp/one.csv","w"){|one| one.puts "key,value1" } File.open("/tmp/two.csv","w") do |two| two.puts "key,value2" two.puts "key2,value_two" end end it "when the key is in the first file" do + @scope.stubs(:lookupvar).with('extlookup_precedence').returns(["one","two"]) result = @scope.function_extlookup([ "key" ]) result.should == "value1" end it "when the key is in the second file" do + @scope.stubs(:lookupvar).with('extlookup_precedence').returns(["one","two"]) result = @scope.function_extlookup([ "key2" ]) result.should == "value_two" end + + it "should not modify extlookup_precedence data" do + variable = '%{fqdn}' + @scope.stubs(:lookupvar).with('extlookup_precedence').returns([variable,"one"]) + @scope.stubs(:lookupvar).with('fqdn').returns('myfqdn') + result = @scope.function_extlookup([ "key" ]) + variable.should == '%{fqdn}' + end end end