diff --git a/lib/puppet/context.rb b/lib/puppet/context.rb index 369027599..f01a399b3 100644 --- a/lib/puppet/context.rb +++ b/lib/puppet/context.rb @@ -1,55 +1,60 @@ # Puppet::Context is a system for tracking services and contextual information # that puppet needs to be able to run. Values are "bound" in a context when it is created # and cannot be changed; however a child context can be created, using # {#override}, that provides a different value. # +# When binding a {Proc}, the proc is called when the value is looked up, and the result +# is memoized for subsequent lookups. This provides a lazy mechanism that can be used to +# delay expensive production of values until they are needed. +# # @api private class Puppet::Context require 'puppet/context/trusted_information' class UndefinedBindingError < Puppet::Error; end class StackUnderflow < Puppet::Error; end # @api private def initialize(initial_bindings) @stack = [] @table = initial_bindings @description = "root" end # @api private def push(overrides, description = "") @stack.push([@table, @description]) @table = @table.merge(overrides || {}) @description = description end # @api private def pop if @stack.empty? raise(StackUnderflow, "Attempted to pop, but already at root of the context stack.") else (@table, @description) = @stack.pop end end # @api private def lookup(name, &block) if @table.include?(name) - @table[name] + value = @table[name] + value.is_a?(Proc) ? (@table[name] = value.call) : value elsif block block.call else raise UndefinedBindingError, "no '#{name}' in #{@table.inspect} at top of #{@stack.inspect}" end end # @api private def override(bindings, description = "", &block) push(bindings, description) yield ensure pop end end diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index 505192f18..cc707b1b0 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -1,74 +1,97 @@ require 'spec_helper' describe Puppet::Context do let(:context) { Puppet::Context.new({ :testing => "value" }) } context "with the implicit test_helper.rb pushed context" do it "fails to lookup a value that does not exist" do expect { context.lookup("a") }.to raise_error(Puppet::Context::UndefinedBindingError) end it "calls a provided block for a default value when none is found" do expect(context.lookup("a") { "default" }).to eq("default") end it "behaves as if pushed a {} if you push nil" do context.push(nil) expect(context.lookup(:testing)).to eq("value") context.pop end it "fails if you try to pop off the top of the stack" do expect { context.pop }.to raise_error(Puppet::Context::StackUnderflow) end end describe "with additional context" do before :each do context.push("a" => 1) end it "holds values for later lookup" do expect(context.lookup("a")).to eq(1) end it "allows rebinding values in a nested context" do inner = nil context.override("a" => 2) do inner = context.lookup("a") end expect(inner).to eq(2) end it "outer bindings are available in an overridden context" do inner_a = nil inner_b = nil context.override("b" => 2) do inner_a = context.lookup("a") inner_b = context.lookup("b") end expect(inner_a).to eq(1) expect(inner_b).to eq(2) end it "overridden bindings do not exist outside of the override" do context.override("a" => 2) do end expect(context.lookup("a")).to eq(1) end it "overridden bindings do not exist outside of the override even when leaving via an error" do begin context.override("a" => 2) do raise "this should still cause the bindings to leave" end rescue end expect(context.lookup("a")).to eq(1) end end + + context 'support lazy entries' do + it 'by evaluating a bound proc' do + result = nil + context.override(:a => lambda {|| 'yay'}) do + result = context.lookup(:a) + end + expect(result).to eq('yay') + end + + it 'by memoizing the bound value' do + result1 = nil + result2 = nil + original = 'yay' + context.override(:a => lambda {|| tmp = original; original = 'no'; tmp}) do + result1 = context.lookup(:a) + result2 = context.lookup(:a) + end + expect(result1).to eq('yay') + expect(original).to eq('no') + expect(result2).to eq('yay') + end + end end