diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index dc8adb89c..e5a93e9f4 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,123 +1,165 @@ require 'puppet/util/autoload' require 'puppet/parser/scope' require 'monitor' # A module for managing parser functions. Each specified function # is added to a central module that then gets included into the Scope # class. module Puppet::Parser::Functions Environment = Puppet::Node::Environment class << self include Puppet::Util end # This is used by tests def self.reset @functions = Hash.new { |h,k| h[k] = {} }.extend(MonitorMixin) @modules = Hash.new.extend(MonitorMixin) # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| newfunction(level, :doc => "Log a message on the server at level #{level.to_s}.") do |vals| send(level, vals.join(" ")) end end end def self.autoloader unless defined?(@autoloader) @autoloader = Puppet::Util::Autoload.new( self, "puppet/parser/functions", :wrap => false ) end @autoloader end def self.environment_module(env = nil) @modules.synchronize { @modules[ env || Environment.current || Environment.root ] ||= Module.new } end # Create a new function type. def self.newfunction(name, options = {}, &block) name = symbolize(name) - raise Puppet::DevError, "Function #{name} already defined" if functions.include?(name) + raise Puppet::DevError, "Function #{name} already defined" if get_function(name) ftype = options[:type] || :statement unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type #{ftype.inspect}" end fname = "function_#{name}" environment_module.send(:define_method, fname, &block) # Someday we'll support specifying an arity, but for now, nope #functions[name] = {:arity => arity, :type => ftype} - functions[name] = {:type => ftype, :name => fname} - functions[name][:doc] = options[:doc] if options[:doc] + func = {:type => ftype, :name => fname} + func[:doc] = options[:doc] if options[:doc] + + add_function(name, func) + func end # Remove a function added by newfunction def self.rmfunction(name) + Puppet.deprecation_warning "Puppet::Parser::Functions.rmfunction is deprecated and will be removed in 3.0" name = symbolize(name) - raise Puppet::DevError, "Function #{name} is not defined" unless functions.include? name + raise Puppet::DevError, "Function #{name} is not defined" unless get_function(name) - functions.delete name + @functions.synchronize { + @functions[Environment.current].delete(name) + # This seems wrong because it won't delete a function defined on root if + # the current environment is different + #@functions[Environment.root].delete(name) + } fname = "function_#{name}" + # This also only deletes from the module associated with + # Environment.current environment_module.send(:remove_method, fname) end # Determine if a given name is a function def self.function(name) name = symbolize(name) + func = nil @functions.synchronize do - unless functions.include?(name) or functions(Puppet::Node::Environment.root).include?(name) - autoloader.load(name,Environment.current || Environment.root) + unless func = get_function(name) + autoloader.load(name, Environment.current) + func = get_function(name) end end - ( functions(Environment.root)[name] || functions[name] || {:name => false} )[:name] + if func + func[:name] + else + false + end end def self.functiondocs autoloader.loadall ret = "" - functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| + merged_functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| ret += "#{name}\n#{"-" * name.to_s.length}\n" if hash[:doc] ret += Puppet::Util::Docs.scrub(hash[:doc]) else ret += "Undocumented.\n" end ret += "\n\n- *Type*: #{hash[:type]}\n\n" end ret end def self.functions(env = nil) + Puppet.deprecation_warning "Puppet::Parser::Functions.functions is deprecated and will be removed in 3.0" @functions.synchronize { @functions[ env || Environment.current || Environment.root ] } end # Determine if a given function returns a value or not. def self.rvalue?(name) - (functions[symbolize(name)] || {})[:type] == :rvalue + func = get_function(name) + func ? func[:type] == :rvalue : false + end + + + class << self + private + + def merged_functions + @functions.synchronize { + @functions[Environment.root].merge(@functions[Environment.current]) + } + end + + def get_function(name) + name = symbolize(name) + merged_functions[name] + end + + def add_function(name, func) + name = symbolize(name) + @functions.synchronize { + @functions[Environment.current][name] = func + } + end end reset # initialize the class instance variables end diff --git a/spec/integration/parser/functions_spec.rb b/spec/integration/parser/functions_spec.rb index 6a8fbca9c..06fa34d0e 100755 --- a/spec/integration/parser/functions_spec.rb +++ b/spec/integration/parser/functions_spec.rb @@ -1,20 +1,16 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::Functions do - before :each do - Puppet::Parser::Functions.rmfunction("template") if Puppet::Parser::Functions.functions.include?("template") - end - it "should support multiple threads autoloading the same function" do threads = [] lambda { 10.times { |a| threads << Thread.new { Puppet::Parser::Functions.function("template") } } }.should_not raise_error threads.each { |t| t.join } end end diff --git a/spec/unit/parser/functions_spec.rb b/spec/unit/parser/functions_spec.rb index 8240a184c..e910a95eb 100755 --- a/spec/unit/parser/functions_spec.rb +++ b/spec/unit/parser/functions_spec.rb @@ -1,101 +1,151 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::Functions do - after(:each) do - # Rationale: - # our various tests will almost all register to Pupet::Parser::Functions - # a new function called "name". All tests are required to stub Puppet::Parser::Scope - # so that +no+ new real ruby method are defined. - # After each test, we want to leave the whole Puppet::Parser::Functions environment - # as it was before we were called, hence we call rmfunction (which might not succeed - # if the function hasn't been registered in the test). It is also important in this - # section to stub +remove_method+ here so that we don't pollute the scope. - Puppet::Parser::Scope.stubs(:remove_method) - begin - Puppet::Parser::Functions.rmfunction("name") - rescue - end - end - it "should have a method for returning an environment-specific module" do Puppet::Parser::Functions.environment_module("myenv").should be_instance_of(Module) end it "should use the current default environment if no environment is provided" do Puppet::Parser::Functions.environment_module.should be_instance_of(Module) end describe "when calling newfunction" do before do @module = Module.new Puppet::Parser::Functions.stubs(:environment_module).returns @module end it "should create the function in the environment module" do @module.expects(:define_method).with { |name,block| name == "function_name" } Puppet::Parser::Functions.newfunction("name", :type => :rvalue) end - it "should raise an error if the function already exists" do - @module.expects(:define_method).with { |name,block| name == "function_name" }.once - Puppet::Parser::Functions.newfunction("name", :type => :rvalue) - - lambda { Puppet::Parser::Functions.newfunction("name", :type => :rvalue) }.should raise_error - end - it "should raise an error if the function type is not correct" do @module.expects(:define_method).with { |name,block| name == "function_name" }.never lambda { Puppet::Parser::Functions.newfunction("name", :type => :unknown) }.should raise_error end end describe "when calling rmfunction" do before do @module = Module.new Puppet::Parser::Functions.stubs(:environment_module).returns @module end it "should remove the function in the scope class" do @module.expects(:define_method).with { |name,block| name == "function_name" } Puppet::Parser::Functions.newfunction("name", :type => :rvalue) @module.expects(:remove_method).with("function_name").once Puppet::Parser::Functions.rmfunction("name") end it "should raise an error if the function doesn't exists" do lambda { Puppet::Parser::Functions.rmfunction("name") }.should raise_error end end describe "when calling function to test function existance" do before do @module = Module.new Puppet::Parser::Functions.stubs(:environment_module).returns @module end it "should return false if the function doesn't exist" do Puppet::Parser::Functions.autoloader.stubs(:load) Puppet::Parser::Functions.function("name").should be_false end it "should return its name if the function exists" do @module.expects(:define_method).with { |name,block| name == "function_name" } Puppet::Parser::Functions.newfunction("name", :type => :rvalue) Puppet::Parser::Functions.function("name").should == "function_name" end it "should try to autoload the function if it doesn't exist yet" do Puppet::Parser::Functions.autoloader.expects(:load) Puppet::Parser::Functions.function("name") end end + + describe "::get_function" do + it "can retrieve a function defined on the *root* environment" do + Thread.current[:environment] = nil + function = Puppet::Parser::Functions.newfunction("atest", :type => :rvalue) do + nil + end + + Puppet::Node::Environment.current = "test_env" + Puppet::Parser::Functions.send(:get_function, "atest").should equal(function) + end + + it "can retrieve a function from the current environment" do + Puppet::Node::Environment.current = "test_env" + function = Puppet::Parser::Functions.newfunction("atest", :type => :rvalue) do + nil + end + + Puppet::Parser::Functions.send(:get_function, "atest").should equal(function) + end + + it "takes a function in the current environment over one in the root" do + root = Puppet::Node::Environment.root + env = Puppet::Node::Environment.current = "test_env" + func1 = {:type => :rvalue, :name => :testfunc, :extra => :func1} + func2 = {:type => :rvalue, :name => :testfunc, :extra => :func2} + Puppet::Parser::Functions.instance_eval do + @functions[Puppet::Node::Environment.root][:atest] = func1 + @functions[Puppet::Node::Environment.current][:atest] = func2 + end + + Puppet::Parser::Functions.send(:get_function, "atest").should equal(func2) + end + end + + describe "::merged_functions" do + it "returns functions in both the current and root environment" do + Thread.current[:environment] = nil + func_a = Puppet::Parser::Functions.newfunction("test_a", :type => :rvalue) do + nil + end + Puppet::Node::Environment.current = "test_env" + func_b = Puppet::Parser::Functions.newfunction("test_b", :type => :rvalue) do + nil + end + + Puppet::Parser::Functions.send(:merged_functions).should include(:test_a, :test_b) + end + + it "returns functions from the current environment over the root environment" do + root = Puppet::Node::Environment.root + env = Puppet::Node::Environment.current = "test_env" + func1 = {:type => :rvalue, :name => :testfunc, :extra => :func1} + func2 = {:type => :rvalue, :name => :testfunc, :extra => :func2} + Puppet::Parser::Functions.instance_eval do + @functions[Puppet::Node::Environment.root][:atest] = func1 + @functions[Puppet::Node::Environment.current][:atest] = func2 + end + + Puppet::Parser::Functions.send(:merged_functions)[:atest].should equal(func2) + end + end + + describe "::add_function" do + it "adds functions to the current environment" do + func = {:type => :rvalue, :name => :testfunc} + Puppet::Node::Environment.current = "add_function_test" + Puppet::Parser::Functions.send(:add_function, :testfunc, func) + + Puppet::Parser::Functions.instance_variable_get(:@functions)[Puppet::Node::Environment.root].should_not include(:testfunc) + Puppet::Parser::Functions.instance_variable_get(:@functions)[Puppet::Node::Environment.current].should include(:testfunc) + end + end end