diff --git a/Gemfile b/Gemfile index ece3ab193..4b85d0efd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,93 +1,93 @@ source ENV['GEM_SOURCE'] || "https://rubygems.org" def location_for(place, fake_version = nil) if place =~ /^(git[:@][^#]*)#(.*)/ [fake_version, { :git => $1, :branch => $2, :require => false }].compact elsif place =~ /^file:\/\/(.*)/ ['>= 0', { :path => File.expand_path($1), :require => false }] else [place, { :require => false }] end end # C Ruby (MRI) or Rubinius, but NOT Windows platforms :ruby do gem 'pry', :group => :development gem 'yard', :group => :development gem 'redcarpet', '~> 2.0', :group => :development gem "racc", "1.4.9", :group => :development # To enable the augeas feature, use this gem. # Note that it is a native gem, so the augeas headers/libs # are neeed. #gem 'ruby-augeas', :group => :development end gem "puppet", :path => File.dirname(__FILE__), :require => false gem "facter", *location_for(ENV['FACTER_LOCATION'] || ['> 1.6', '< 3']) gem "hiera", *location_for(ENV['HIERA_LOCATION'] || '~> 1.0') gem "rake", "10.1.1", :require => false -gem "rgen", "0.6.5", :require => false +gem "rgen", "0.7.0", :require => false group(:development, :test) do gem "rspec", "~> 2.11.0", :require => false # Mocha is not compatible across minor version changes; because of this only # versions matching ~> 0.10.5 are supported. All other versions are unsupported # and can be expected to fail. gem "mocha", "~> 0.10.5", :require => false gem "yarjuf", "~> 1.0" # json-schema does not support windows, so omit it from the platforms list # json-schema uses multi_json, but chokes with multi_json 1.7.9, so prefer 1.7.7 gem "multi_json", "1.7.7", :require => false, :platforms => [:ruby, :jruby] gem "json-schema", "2.1.1", :require => false, :platforms => [:ruby, :jruby] end group(:development) do case RUBY_VERSION when /^1.8/ gem 'ruby-prof', "~> 0.13.1", :require => false else gem 'ruby-prof', :require => false end end group(:extra) do gem "rack", "~> 1.4", :require => false gem "activerecord", '~> 3.2', :require => false gem "couchrest", '~> 1.0', :require => false gem "net-ssh", '~> 2.1', :require => false gem "puppetlabs_spec_helper", :require => false gem "stomp", :require => false gem "tzinfo", :require => false case RUBY_PLATFORM when 'java' gem "jdbc-sqlite3", :require => false gem "msgpack-jruby", :require => false else gem "sqlite3", :require => false gem "msgpack", :require => false end end require 'yaml' data = YAML.load_file(File.join(File.dirname(__FILE__), 'ext', 'project_data.yaml')) bundle_platforms = data['bundle_platforms'] data['gem_platform_dependencies'].each_pair do |gem_platform, info| if bundle_deps = info['gem_runtime_dependencies'] bundle_platform = bundle_platforms[gem_platform] or raise "Missing bundle_platform" platform(bundle_platform.intern) do bundle_deps.each_pair do |name, version| gem(name, version, :require => false) end end end end if File.exists? "#{__FILE__}.local" eval(File.read("#{__FILE__}.local"), binding) end # vim:filetype=ruby diff --git a/lib/puppet/functions.rb b/lib/puppet/functions.rb index de136e7e6..1996d91ff 100644 --- a/lib/puppet/functions.rb +++ b/lib/puppet/functions.rb @@ -1,548 +1,550 @@ # @note WARNING: This new function API is still under development and may change at any time # # Functions in the puppet language can be written in Ruby and distributed in # puppet modules. The function is written by creating a file in the module's # `lib/puppet/functions/` directory, where `` is # replaced with the module's name. The file should have the name of the function. # For example, to create a function named `min` in a module named `math` create # a file named `lib/puppet/functions/math/min.rb` in the module. # # A function is implemented by calling {Puppet::Functions.create_function}, and # passing it a block that defines the implementation of the function. # # Functions are namespaced inside the module that contains them. The name of # the function is prefixed with the name of the module. For example, # `math::min`. # # @example A simple function # Puppet::Functions.create_function('math::min') do # def min(a, b) # a <= b ? a : b # end # end # # Anatomy of a function # --- # # Functions are composed of four parts: the name, the implementation methods, # the signatures, and the dispatches. # # The name is the string given to the {Puppet::Functions.create_function} # method. It specifies the name to use when calling the function in the puppet # language, or from other functions. # # The implementation methods are ruby methods (there can be one or more) that # provide that actual implementation of the function's behavior. In the # simplest case the name of the function (excluding any namespace) and the name # of the method are the same. When that is done no other parts (signatures and # dispatches) need to be used. # # Signatures are a way of specifying the types of the function's parameters. # The types of any arguments will be checked against the types declared in the # signature and an error will be produced if they don't match. The types are # defined by using the same syntax for types as in the puppet language. # # Dispatches are how signatures and implementation methods are tied together. # When the function is called, puppet searches the signatures for one that # matches the supplied arguments. Each signature is part of a dispatch, which # specifies the method that should be called for that signature. When a # matching signature is found, the corrosponding method is called. # # Documentation for the function should be placed as comments to the # implementation method(s). # # @todo Documentation for individual instances of these new functions is not # yet tied into the puppet doc system. # # @example Dispatching to different methods by type # Puppet::Functions.create_function('math::min') do # dispatch :numeric_min do # param 'Numeric', 'a' # param 'Numeric', 'b' # end # # dispatch :string_min do # param 'String', 'a' # param 'String', 'b' # end # # def numeric_min(a, b) # a <= b ? a : b # end # # def string_min(a, b) # a.downcase <= b.downcase ? a : b # end # end # # Specifying Signatures # --- # # If nothing is specified, the number of arguments given to the function must # be the same as the number of parameters, and all of the parameters are of # type 'Any'. # # To express that the last parameter captures the rest, the method # `last_captures_rest` can be called. This indicates that the last parameter is # a varargs parameter and will be passed to the implementing method as an array # of the given type. # # When defining a dispatch for a function, the resulting dispatch matches # against the specified argument types and min/max occurrence of optional # entries. When the dispatch makes the call to the implementation method the # arguments are simply passed and it is the responsibility of the method's # implementor to ensure it can handle those arguments (i.e. there is no check # that what was declared as optional actually has a default value, and that # a "captures rest" is declared using a `*`). # # @example Varargs # Puppet::Functions.create_function('foo') do # dispatch :foo do # param 'Numeric', 'first' # param 'Numeric', 'values' # last_captures_rest # end # # def foo(first, *values) # # do something # end # end # # Access to Scope # --- # In general, functions should not need access to scope; they should be # written to act on their given input only. If they absolutely must look up # variable values, they should do so via the closure scope (the scope where # they are defined) - this is done by calling `closure_scope()`. # # Calling other Functions # --- # Calling other functions by name is directly supported via # {Puppet::Pops::Functions::Function#call_function}. This allows a function to # call other functions visible from its loader. # # @api public module Puppet::Functions # @param func_name [String, Symbol] a simple or qualified function name # @param block [Proc] the block that defines the methods and dispatch of the # Function to create # @return [Class] the newly created Function class # # @api public def self.create_function(func_name, function_base = Function, &block) if function_base.ancestors.none? { |s| s == Puppet::Pops::Functions::Function } raise ArgumentError, "Functions must be based on Puppet::Pops::Functions::Function. Got #{function_base}" end func_name = func_name.to_s # Creates an anonymous class to represent the function # The idea being that it is garbage collected when there are no more # references to it. # the_class = Class.new(function_base, &block) # Make the anonymous class appear to have the class-name # Even if this class is not bound to such a symbol in a global ruby scope and # must be resolved via the loader. # This also overrides any attempt to define a name method in the given block # (Since it redefines it) # # TODO, enforce name in lower case (to further make it stand out since Ruby # class names are upper case) # the_class.instance_eval do @func_name = func_name def name @func_name end end # Automatically create an object dispatcher based on introspection if the # loaded user code did not define any dispatchers. Fail if function name # does not match a given method name in user code. # if the_class.dispatcher.empty? simple_name = func_name.split(/::/)[-1] type, names = default_dispatcher(the_class, simple_name) last_captures_rest = (type.size_range[1] == Puppet::Pops::Types::INFINITY) the_class.dispatcher.add_dispatch(type, simple_name, names, nil, nil, nil, last_captures_rest) end # The function class is returned as the result of the create function method the_class end # Creates a default dispatcher configured from a method with the same name as the function # # @api private def self.default_dispatcher(the_class, func_name) unless the_class.method_defined?(func_name) raise ArgumentError, "Function Creation Error, cannot create a default dispatcher for function '#{func_name}', no method with this name found" end any_signature(*min_max_param(the_class.instance_method(func_name))) end # @api private def self.min_max_param(method) # Ruby 1.8.7 does not have support for details about parameters if method.respond_to?(:parameters) result = {:req => 0, :opt => 0, :rest => 0 } # TODO: Optimize into one map iteration that produces names map, and sets # count as side effect method.parameters.each { |p| result[p[0]] += 1 } from = result[:req] to = result[:rest] > 0 ? :default : from + result[:opt] names = method.parameters.map {|p| p[1].to_s } else # Cannot correctly compute the signature in Ruby 1.8.7 because arity for # optional values is screwed up (there is no way to get the upper limit), # an optional looks the same as a varargs In this case - the failure will # simply come later when the call fails # arity = method.arity from = arity >= 0 ? arity : -arity -1 to = arity >= 0 ? arity : :default # i.e. infinite (which is wrong when there are optional - flaw in 1.8.7) names = [] # no names available end [from, to, names] end # Construct a signature consisting of Object type, with min, and max, and given names. # (there is only one type entry). # # @api private def self.any_signature(from, to, names) # Construct the type for the signature # Tuple[Object, from, to] factory = Puppet::Pops::Types::TypeFactory [factory.callable(factory.any, from, to), names] end # Function # === # This class is the base class for all Puppet 4x Function API functions. A # specialized class is created for each puppet function. # # @api public class Function < Puppet::Pops::Functions::Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables DispatcherBuilder.new(dispatcher, @type_parser, @all_callables) end # Dispatch any calls that match the signature to the provided method name. # # @param meth_name [Symbol] The name of the implementation method to call # when the signature defined in the block matches the arguments to a call # to the function. # @return [Void] # # @api public def self.dispatch(meth_name, &block) builder().instance_eval do dispatch(meth_name, &block) end end end # Public api methods of the DispatcherBuilder are available within dispatch() # blocks declared in a Puppet::Function.create_function() call. # # @api public class DispatcherBuilder # @api private def initialize(dispatcher, type_parser, all_callables) @type_parser = type_parser @all_callables = all_callables @dispatcher = dispatcher end # Defines a positional parameter with type and name # # @param type [String] The type specification for the parameter. # @param name [String] The name of the parameter. This is primarily used # for error message output and does not have to match the name of the # parameter on the implementation method. # @return [Void] # # @api public def param(type, name) if type.is_a?(String) @types << type @names << name # mark what should be picked for this position when dispatching @weaving << @names.size()-1 else raise ArgumentError, "Type signature argument must be a String reference to a Puppet Data Type. Got #{type.class}" end end # Defines one required block parameter that may appear last. If type and name is missing the # default type is "Callable", and the name is "block". If only one # parameter is given, then that is the name and the type is "Callable". # # @api public def required_block_param(*type_and_name) case type_and_name.size when 0 - type = @all_callables + # the type must be an independent instance since it will be contained in another type + type = @all_callables.copy name = 'block' when 1 - type = @all_callables + # the type must be an independent instance since it will be contained in another type + type = @all_callables.copy name = type_and_name[0] when 2 type_string, name = type_and_name type = @type_parser.parse(type_string) else raise ArgumentError, "block_param accepts max 2 arguments (type, name), got #{type_and_name.size}." end unless type.is_a?(Puppet::Pops::Types::PCallableType) raise ArgumentError, "Expected PCallableType, got #{type.class}" end unless name.is_a?(String) raise ArgumentError, "Expected block_param name to be a String, got #{name.class}" end if @block_type.nil? @block_type = type @block_name = name else raise ArgumentError, "Attempt to redefine block" end end # Defines one optional block parameter that may appear last. If type or name is missing the # defaults are "any callable", and the name is "block". The implementor of the dispatch target # must use block = nil when it is optional (or an error is raised when the call is made). # # @api public def optional_block_param(*type_and_name) # same as required, only wrap the result in an optional type required_block_param(*type_and_name) @block_type = Puppet::Pops::Types::TypeFactory.optional(@block_type) end # Specifies the min and max occurance of arguments (of the specified types) # if something other than the exact count from the number of specified # types). The max value may be specified as -1 if an infinite number of # arguments are supported. When max is > than the number of specified # types, the last specified type repeats. # # @api public def arg_count(min_occurs, max_occurs) @min = min_occurs @max = max_occurs unless min_occurs.is_a?(Integer) && min_occurs >= 0 raise ArgumentError, "min arg_count of function parameter must be an Integer >=0, got #{min_occurs.class} '#{min_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= 0) raise ArgumentError, "max arg_count of function parameter must be an Integer >= 0, or :default, got #{max_occurs.class} '#{max_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= min_occurs) raise ArgumentError, "max arg_count must be :default (infinite) or >= min arg_count, got min: '#{min_occurs}, max: '#{max_occurs}'" end end # Specifies that the last argument captures the rest. # # @api public def last_captures_rest @last_captures = true end private # @api private def dispatch(meth_name, &block) # an array of either an index into names/types, or an array with # injection information [type, name, injection_name] used when the call # is being made to weave injections into the given arguments. # @types = [] @names = [] @weaving = [] @injections = [] @min = nil @max = nil @last_captures = false @block_type = nil @block_name = nil self.instance_eval &block callable_t = create_callable(@types, @block_type, @min, @max) @dispatcher.add_dispatch(callable_t, meth_name, @names, @block_name, @injections, @weaving, @last_captures) end # Handles creation of a callable type from strings specifications of puppet # types and allows the min/max occurs of the given types to be given as one # or two integer values at the end. The given block_type should be # Optional[Callable], Callable, or nil. # # @api private def create_callable(types, block_type, from, to) mapped_types = types.map do |t| @type_parser.parse(t) end if !(from.nil? && to.nil?) mapped_types << from mapped_types << to end if block_type mapped_types << block_type end Puppet::Pops::Types::TypeFactory.callable(*mapped_types) end end private # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # This is a private, internal, system for creating functions. It supports # everything that the public function definition system supports as well as a # few extra features. # # Injection Support # === # The Function API supports injection of data and services. It is possible to # make injection that takes effect when the function is loaded (for services # and runtime configuration that does not change depending on how/from where # in what context the function is called. It is also possible to inject and # weave argument values into a call. # # Injection of attributes # --- # Injection of attributes is performed by one of the methods `attr_injected`, # and `attr_injected_producer`. The injected attributes are available via # accessor method calls. # # @example using injected attributes # Puppet::Functions.create_function('test') do # attr_injected String, :larger, 'message_larger' # attr_injected String, :smaller, 'message_smaller' # def test(a, b) # a > b ? larger() : smaller() # end # end # # @api private class InternalFunction < Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables InternalDispatchBuilder.new(dispatcher, @type_parser, @all_callables) end # Defines class level injected attribute with reader method # # @api private def self.attr_injected(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end # Defines class level injected producer attribute with reader method # # @api private def self.attr_injected_producer(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup_producer(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end end # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # Injection and Weaving of parameters # --- # It is possible to inject and weave parameters into a call. These extra # parameters are not part of the parameters passed from the Puppet logic, and # they can not be overridden by parameters given as arguments in the call. # They are invisible to the Puppet Language. # # @example using injected parameters # Puppet::Functions.create_function('test') do # dispatch :test do # param 'Scalar', 'a' # param 'Scalar', 'b' # injected_param 'String', 'larger', 'message_larger' # injected_param 'String', 'smaller', 'message_smaller' # end # def test(a, b, larger, smaller) # a > b ? larger : smaller # end # end # # The function in the example above is called like this: # # test(10, 20) # # Using injected value as default # --- # Default value assignment is handled by using the regular Ruby mechanism (a # value is assigned to the variable). The dispatch simply indicates that the # value is optional. If the default value should be injected, it can be # handled different ways depending on what is desired: # # * by calling the accessor method for an injected Function class attribute. # This is suitable if the value is constant across all instantiations of the # function, and across all calls. # * by injecting a parameter into the call # to the left of the parameter, and then assigning that as the default value. # * One of the above forms, but using an injected producer instead of a # directly injected value. # # @example method with injected default values # Puppet::Functions.create_function('test') do # dispatch :test do # injected_param String, 'b_default', 'b_default_value_key' # param 'Scalar', 'a' # param 'Scalar', 'b' # end # def test(b_default, a, b = b_default) # # ... # end # end # # @api private class InternalDispatchBuilder < DispatcherBuilder # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_param(type, name, injection_name = '') @injections << [type, name, injection_name] # mark what should be picked for this position when dispatching @weaving << [@injections.size() -1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_producer_param(type, name, injection_name = '') @injections << [type, name, injection_name, :producer] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end end end diff --git a/spec/unit/pops/binder/injector_spec.rb b/spec/unit/pops/binder/injector_spec.rb index 32eba3260..3905f92d3 100644 --- a/spec/unit/pops/binder/injector_spec.rb +++ b/spec/unit/pops/binder/injector_spec.rb @@ -1,784 +1,787 @@ require 'spec_helper' require 'puppet/pops' module InjectorSpecModule def injector(binder) Puppet::Pops::Binder::Injector.new(binder) end def factory Puppet::Pops::Binder::BindingsFactory end def test_layer_with_empty_bindings factory.named_layer('test-layer', factory.named_bindings('test').model) end def test_layer_with_bindings(*bindings) factory.named_layer('test-layer', *bindings) end def null_scope() nil end def type_calculator Puppet::Pops::Types::TypeCalculator end def type_factory Puppet::Pops::Types::TypeFactory end # Returns a binder # def configured_binder b = Puppet::Pops::Binder::Binder.new() b end class TestDuck end class Daffy < TestDuck end class AngryDuck < TestDuck # Supports assisted inject, returning a Donald duck as the default impl of Duck def self.inject(injector, scope, binding, *args) Donald.new() end end class Donald < AngryDuck end class ArneAnka < AngryDuck attr_reader :label def initialize() @label = 'A Swedish angry cartoon duck' end end class ScroogeMcDuck < TestDuck attr_reader :fortune # Supports assisted inject, returning an ScroogeMcDuck with 1$ fortune or first arg in args # Note that when injected (via instance producer, or implict assisted inject, the inject method # always wins. def self.inject(injector, scope, binding, *args) self.new(args[0].nil? ? 1 : args[0]) end def initialize(fortune) @fortune = fortune end end class NamedDuck < TestDuck attr_reader :name def initialize(name) @name = name end end # Test custom producer that on each produce returns a duck that is twice as rich as its predecessor class ScroogeProducer < Puppet::Pops::Binder::Producers::Producer attr_reader :next_capital def initialize @next_capital = 100 end def produce(scope) ScroogeMcDuck.new(@next_capital *= 2) end end end describe 'Injector' do include InjectorSpecModule let(:bindings) { factory.named_bindings('test') } let(:scope) { null_scope()} - let(:duck_type) { type_factory.ruby(InjectorSpecModule::TestDuck) } +# let(:duck_type) { type_factory.ruby(InjectorSpecModule::TestDuck) } let(:binder) { Puppet::Pops::Binder::Binder } let(:lbinder) do binder.new(layered_bindings) end + def duck_type + type_factory.ruby(InjectorSpecModule::TestDuck) + end let(:layered_bindings) { factory.layered_bindings(test_layer_with_bindings(bindings.model)) } context 'When created' do it 'should not raise an error if binder is configured' do expect { injector(lbinder) }.to_not raise_error end it 'should create an empty injector given an empty binder' do expect { binder.new(layered_bindings) }.to_not raise_exception end it "should be possible to reference the TypeCalculator" do injector(lbinder).type_calculator.is_a?(Puppet::Pops::Types::TypeCalculator).should == true end it "should be possible to reference the KeyFactory" do injector(lbinder).key_factory.is_a?(Puppet::Pops::Binder::KeyFactory).should == true end it "can be created using a model" do bindings.bind.name('a_string').to('42') injector = Puppet::Pops::Binder::Injector.create_from_model(layered_bindings) injector.lookup(scope, 'a_string').should == '42' end it 'can be created using a block' do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('42') end injector.lookup(scope, 'a_string').should == '42' end it 'can be created using a hash' do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') injector.lookup(scope, 'a_string').should == '42' end it 'can be created using an overriding injector with block' do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('42') end injector2 = injector.override('override') do bind.name('a_string').to('43') end injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end it 'can be created using an overriding injector with hash' do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') injector2 = injector.override_with_hash('override', 'a_string' => '43') injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end it "can be created using an overriding injector with a model" do injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42') bindings.bind.name('a_string').to('43') injector2 = injector.override_with_model(layered_bindings) injector.lookup(scope, 'a_string').should == '42' injector2.lookup(scope, 'a_string').should == '43' end end context "When looking up objects" do it 'lookup(scope, name) finds bound object of type Data with given name' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string').should == '42' end context 'a block transforming the result can be given' do it 'that transform a found value given scope and value' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string') {|zcope, val| val + '42' }.should == '4242' end it 'that transform a found value given only value' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_string') {|val| val + '42' }.should == '4242' end it 'that produces a default value when entry is missing' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup(scope, 'a_non_existing_string') {|val| val ? (raise Error, "Should not happen") : '4242' }.should == '4242' end end context "and class is not bound" do it "assisted inject kicks in for classes with zero args constructor" do duck_type = type_factory.ruby(InjectorSpecModule::Daffy) injector = injector(lbinder) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Daffy).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Daffy).should == true end it "assisted inject produces same instance on lookup but not on lookup producer" do duck_type = type_factory.ruby(InjectorSpecModule::Daffy) injector = injector(lbinder) d1 = injector.lookup(scope, duck_type) d2 = injector.lookup(scope, duck_type) d1.equal?(d2).should == true d1 = injector.lookup_producer(scope, duck_type).produce(scope) d2 = injector.lookup_producer(scope, duck_type).produce(scope) d1.equal?(d2).should == false end it "assisted inject kicks in for classes with a class inject method" do duck_type = type_factory.ruby(InjectorSpecModule::ScroogeMcDuck) injector = injector(lbinder) # Do not pass any arguments, the ScroogeMcDuck :inject method should pick 1 by default # This tests zero args passed injector.lookup(scope, duck_type).fortune.should == 1 injector.lookup_producer(scope, duck_type).produce(scope).fortune.should == 1 end it "assisted inject selects the inject method if it exists over a zero args constructor" do injector = injector(lbinder) duck_type = type_factory.ruby(InjectorSpecModule::AngryDuck) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Donald).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Donald).should == true end it "assisted inject selects the zero args constructor if injector is from a superclass" do injector = injector(lbinder) duck_type = type_factory.ruby(InjectorSpecModule::ArneAnka) injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::ArneAnka).should == true injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::ArneAnka).should == true end end context "and multiple layers are in use" do it "a higher layer shadows anything in a lower layer" do bindings1 = factory.named_bindings('test1') bindings1.bind().name('a_string').to('bad stuff') lower_layer = factory.named_layer('lower-layer', bindings1.model) bindings2 = factory.named_bindings('test2') bindings2.bind().name('a_string').to('good stuff') higher_layer = factory.named_layer('higher-layer', bindings2.model) injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer))) injector.lookup(scope,'a_string').should == 'good stuff' end it "a higher layer may not shadow a lower layer binding that is final" do bindings1 = factory.named_bindings('test1') bindings1.bind().final.name('a_string').to('required stuff') lower_layer = factory.named_layer('lower-layer', bindings1.model) bindings2 = factory.named_bindings('test2') bindings2.bind().name('a_string').to('contraband') higher_layer = factory.named_layer('higher-layer', bindings2.model) expect { injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer))) }.to raise_error(/Override of final binding not allowed/) end end context "and dealing with Data types" do let(:lbinder) { binder.new(layered_bindings) } it "should treat all data as same type w.r.t. key" do bindings.bind().name('a_string').to('42') bindings.bind().name('an_int').to(43) bindings.bind().name('a_float').to(3.14) bindings.bind().name('a_boolean').to(true) bindings.bind().name('an_array').to([1,2,3]) bindings.bind().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3}) injector = injector(lbinder) injector.lookup(scope,'a_string').should == '42' injector.lookup(scope,'an_int').should == 43 injector.lookup(scope,'a_float').should == 3.14 injector.lookup(scope,'a_boolean').should == true injector.lookup(scope,'an_array').should == [1,2,3] injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} end it "should provide type-safe lookup of given type/name" do bindings.bind().string().name('a_string').to('42') bindings.bind().integer().name('an_int').to(43) bindings.bind().float().name('a_float').to(3.14) bindings.bind().boolean().name('a_boolean').to(true) bindings.bind().array_of_data().name('an_array').to([1,2,3]) bindings.bind().hash_of_data().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3}) injector = injector(lbinder) # Check lookup using implied Data type injector.lookup(scope,'a_string').should == '42' injector.lookup(scope,'an_int').should == 43 injector.lookup(scope,'a_float').should == 3.14 injector.lookup(scope,'a_boolean').should == true injector.lookup(scope,'an_array').should == [1,2,3] injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} # Check lookup using expected type injector.lookup(scope,type_factory.string(), 'a_string').should == '42' injector.lookup(scope,type_factory.integer(), 'an_int').should == 43 injector.lookup(scope,type_factory.float(),'a_float').should == 3.14 injector.lookup(scope,type_factory.boolean(),'a_boolean').should == true injector.lookup(scope,type_factory.array_of_data(),'an_array').should == [1,2,3] injector.lookup(scope,type_factory.hash_of_data(),'a_hash').should == {'a'=>1,'b'=>2,'c'=>3} # Check lookup using wrong type expect { injector.lookup(scope,type_factory.integer(), 'a_string')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(), 'an_int')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_float')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_boolean')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'an_array')}.to raise_error(/Type error/) expect { injector.lookup(scope,type_factory.string(),'a_hash')}.to raise_error(/Type error/) end end end context "When looking up producer" do it 'the value is produced by calling produce(scope)' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string').produce(scope).should == '42' end context 'a block transforming the result can be given' do it 'that transform a found value given scope and producer' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string') {|zcope, p| p.produce(zcope) + '42' }.should == '4242' end it 'that transform a found value given only producer' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_string') {|p| p.produce(scope) + '42' }.should == '4242' end it 'that can produce a default value when entry is not found' do bindings.bind().name('a_string').to('42') injector(lbinder).lookup_producer(scope, 'a_non_existing_string') {|p| p ? (raise Error,"Should not happen") : '4242' }.should == '4242' end end end context "When dealing with singleton vs. non singleton" do it "should produce the same instance when producer is a singleton" do bindings.bind().name('a_string').to('42') injector = injector(lbinder) a = injector.lookup(scope, 'a_string') b = injector.lookup(scope, 'a_string') a.equal?(b).should == true end it "should produce different instances when producer is a non singleton producer" do bindings.bind().name('a_string').to_series_of('42') injector = injector(lbinder) a = injector.lookup(scope, 'a_string') b = injector.lookup(scope, 'a_string') a.should == '42' b.should == '42' a.equal?(b).should == false end end context "When using the lookup producer" do it "should lookup again to produce a value" do bindings.bind().name('a_string').to_lookup_of('another_string') bindings.bind().name('another_string').to('hello') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end it "should produce nil if looked up key does not exist" do bindings.bind().name('a_string').to_lookup_of('non_existing') injector(lbinder).lookup(scope, 'a_string').should == nil end it "should report an error if lookup loop is detected" do bindings.bind().name('a_string').to_lookup_of('a_string') expect { injector(lbinder).lookup(scope, 'a_string') }.to raise_error(/Lookup loop/) end end context "When using the hash lookup producer" do it "should lookup a key in looked up hash" do data_hash = type_factory.hash_of_data() bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'a_hash', 'huey') bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'}) injector(lbinder).lookup(scope, 'a_string').should == 'red' end it "should produce nil if looked up entry does not exist" do data_hash = type_factory.hash_of_data() bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'non_existing_entry', 'huey') bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'}) injector(lbinder).lookup(scope, 'a_string').should == nil end end context "When using the first found producer" do it "should lookup until it finds a value, but not further" do bindings.bind().name('a_string').to_first_found('b_string', 'c_string', 'g_string') bindings.bind().name('c_string').to('hello') bindings.bind().name('g_string').to('Oh, mrs. Smith...') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end it "should lookup until it finds a value using mix of type and name, but not further" do bindings.bind().name('a_string').to_first_found('b_string', [type_factory.string, 'c_string'], 'g_string') bindings.bind().name('c_string').to('hello') bindings.bind().name('g_string').to('Oh, mrs. Smith...') injector(lbinder).lookup(scope, 'a_string').should == 'hello' end end context "When producing instances" do it "should lookup an instance of a class without arguments" do bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::Daffy) injector(lbinder).lookup(scope, duck_type, 'the_duck').is_a?(InjectorSpecModule::Daffy).should == true end it "should lookup an instance of a class with arguments" do bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::ScroogeMcDuck, 1234) injector = injector(lbinder) the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 1234 end it "singleton producer should not be recreated between lookups" do bindings.bind().type(duck_type).name('the_duck').to_producer(InjectorSpecModule::ScroogeProducer) injector = injector(lbinder) the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 200 # singleton, do it again to get next value in series - it is the producer that is a singleton # not the produced value the_duck = injector.lookup(scope, duck_type, 'the_duck') the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true the_duck.fortune.should == 400 duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune.should == 800 end it "series of producers should recreate producer on each lookup and lookup_producer" do bindings.bind().type(duck_type).name('the_duck').to_producer_series(InjectorSpecModule::ScroogeProducer) injector = injector(lbinder) duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune().should == 200 duck_producer.produce(scope).fortune().should == 400 # series, each lookup gets a new producer (initialized to produce 200) duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck') duck_producer.produce(scope).fortune().should == 200 duck_producer.produce(scope).fortune().should == 400 injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200 injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200 end end context "When working with multibind" do context "of hash kind" do it "a multibind produces contributed items keyed by their bound key-name" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew2').to(InjectorSpecModule::NamedDuck, 'Dewey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew3').to(InjectorSpecModule::NamedDuck, 'Louie') injector = injector(lbinder) the_ducks = injector.lookup(scope, hash_of_duck, "donalds_nephews") the_ducks.size.should == 3 the_ducks['nephew1'].name.should == 'Huey' the_ducks['nephew2'].name.should == 'Dewey' the_ducks['nephew3'].name.should == 'Louie' end it "is an error to not bind contribution with a name" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') # missing name bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to raise_error(/must have a name/) end it "is an error to bind with duplicate key when using default (priority) conflict resolution" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') # missing name bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to raise_error(/Duplicate key/) end it "is not an error to bind with duplicate key when using (ignore) conflict resolution" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews').producer_options(:conflict_resolution => :ignore) bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey') bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey') expect { the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews") }.to_not raise_error(/Duplicate key/) end it "should produce detailed type error message" do hash_of_integer = type_factory.hash_of(type_factory.integer()) multibind_id = "ints" mb = bindings.multibind(multibind_id).type(hash_of_integer).name('donalds_family') bindings.bind.in_multibind(multibind_id).name('nephew').to('Huey') expect { ducks = injector(lbinder).lookup(scope, 'donalds_family') }.to raise_error(%r{expected: Integer, got: String}) end it "should be possible to combine hash multibind contributions with append on conflict" do # This case uses a multibind of individual strings, but combines them # into an array bound to a hash key # (There are other ways to do this - e.g. have the multibind lookup a multibind # of array type to which nephews are contributed). # hash_of_data = type_factory.hash_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family') mb.producer_options(:conflict_resolution => :append) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck') bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake') ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks['nephews'].should == ['Huey', 'Dewey', 'Louie'] ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake'] end it "should be possible to combine hash multibind contributions with append, flat, and uniq, on conflict" do # This case uses a multibind of individual strings, but combines them # into an array bound to a hash key # (There are other ways to do this - e.g. have the multibind lookup a multibind # of array type to which nephews are contributed). # hash_of_data = type_factory.hash_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family') mb.producer_options(:conflict_resolution => :append, :flatten => true, :uniq => true) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to(['Huey', ['Louie'], 'Dewey']) bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck') bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake') ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks['nephews'].should == ['Huey', 'Dewey', 'Louie'] ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake'] end it "should fail attempts to append, perform uniq or flatten on type incompatible multibind hash" do hash_of_integer = type_factory.hash_of(type_factory.integer()) ids = ["ducks1", "ducks2", "ducks3"] - mb = bindings.multibind(ids[0]).type(hash_of_integer).name('broken_family0') + mb = bindings.multibind(ids[0]).type(hash_of_integer.copy).name('broken_family0') mb.producer_options(:conflict_resolution => :append) - mb = bindings.multibind(ids[1]).type(hash_of_integer).name('broken_family1') + mb = bindings.multibind(ids[1]).type(hash_of_integer.copy).name('broken_family1') mb.producer_options(:flatten => :true) - mb = bindings.multibind(ids[2]).type(hash_of_integer).name('broken_family2') + mb = bindings.multibind(ids[2]).type(hash_of_integer.copy).name('broken_family2') mb.producer_options(:uniq => :true) injector = injector(binder.new(factory.layered_bindings(test_layer_with_bindings(bindings.model)))) expect { injector.lookup(scope, 'broken_family0')}.to raise_error(/:conflict_resolution => :append/) expect { injector.lookup(scope, 'broken_family1')}.to raise_error(/:flatten/) expect { injector.lookup(scope, 'broken_family2')}.to raise_error(/:uniq/) end it "a higher priority contribution is selected when resolution is :priority" do hash_of_duck = type_factory.hash_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews') mb1 = bindings.bind.in_multibind(multibind_id) pending 'priority based on layers not added, and priority on category removed' mb1.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Huey') mb2 = bindings.bind.in_multibind(multibind_id) mb2.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Dewey') binder.define_layers(layered_bindings) injector(binder).lookup(scope, hash_of_duck, "donalds_nephews")['nephew'].name.should == 'Huey' end it "a higher priority contribution wins when resolution is :merge" do # THIS TEST MAY DEPEND ON HASH ORDER SINCE PRIORITY BASED ON CATEGORY IS REMOVED hash_of_data = type_factory.hash_of_data() multibind_id = "hashed_ducks" bindings.multibind(multibind_id).type(hash_of_data).name('donalds_nephews').producer_options(:conflict_resolution => :merge) mb1 = bindings.bind.in_multibind(multibind_id) mb1.name('nephew').to({'name' => 'Huey', 'is' => 'winner'}) mb2 = bindings.bind.in_multibind(multibind_id) mb2.name('nephew').to({'name' => 'Dewey', 'is' => 'looser', 'has' => 'cap'}) the_ducks = injector(binder.new(layered_bindings)).lookup(scope, "donalds_nephews"); the_ducks['nephew']['name'].should == 'Huey' the_ducks['nephew']['is'].should == 'winner' the_ducks['nephew']['has'].should == 'cap' end end context "of array kind" do it "an array multibind produces contributed items, names are allowed but ignored" do array_of_duck = type_factory.array_of(duck_type) multibind_id = "ducks" bindings.multibind(multibind_id).type(array_of_duck).name('donalds_nephews') # one with name (ignored, expect no error) bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey') # two without name bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey') bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Louie') the_ducks = injector(lbinder).lookup(scope, array_of_duck, "donalds_nephews") the_ducks.size.should == 3 the_ducks.collect {|d| d.name }.sort.should == ['Dewey', 'Huey', 'Louie'] end it "should be able to make result contain only unique entries" do # This case uses a multibind of individual strings, and combines them # into an array of unique values # array_of_data = type_factory.array_of_data() multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(array_of_data).name('donalds_family') # turn off priority on named to not trigger conflict as all additions have the same precedence # (could have used the default for unnamed and add unnamed entries). mb.producer_options(:priority_on_named => false, :uniq => true) bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') # duplicate bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks.should == ['Huey', 'Dewey', 'Louie'] end it "should be able to contribute elements and arrays of elements and flatten 1 level" do # This case uses a multibind of individual strings and arrays, and combines them # into an array of flattened # array_of_string = type_factory.array_of(type_factory.string()) multibind_id = "ducks" mb = bindings.multibind(multibind_id).type(array_of_string).name('donalds_family') # flatten one level mb.producer_options(:flatten => 1) bindings.bind.in_multibind(multibind_id).to('Huey') bindings.bind.in_multibind(multibind_id).to('Dewey') bindings.bind.in_multibind(multibind_id).to('Louie') # duplicate bindings.bind.in_multibind(multibind_id).to(['Huey', 'Dewey', 'Louie']) ducks = injector(lbinder).lookup(scope, 'donalds_family') ducks.should == ['Huey', 'Dewey', 'Louie', 'Huey', 'Dewey', 'Louie'] end it "should produce detailed type error message" do array_of_integer = type_factory.array_of(type_factory.integer()) multibind_id = "ints" mb = bindings.multibind(multibind_id).type(array_of_integer).name('donalds_family') bindings.bind.in_multibind(multibind_id).to('Huey') expect { ducks = injector(lbinder).lookup(scope, 'donalds_family') }.to raise_error(%r{expected: Integer, or Array\[Integer\], got: String}) end end context "When using multibind in multibind" do it "a hash multibind can be contributed to another" do hash_of_data = type_factory.hash_of_data() mb1_id = 'data1' mb2_id = 'data2' top = bindings.multibind(mb1_id).type(hash_of_data).name("top") detail = bindings.multibind(mb2_id).type(hash_of_data).name("detail").in_multibind(mb1_id) bindings.bind.in_multibind(mb1_id).name('a').to(10) bindings.bind.in_multibind(mb1_id).name('b').to(20) bindings.bind.in_multibind(mb2_id).name('a').to(30) bindings.bind.in_multibind(mb2_id).name('b').to(40) expect( injector(lbinder).lookup(scope, "top") ).to eql({'detail' => {'a' => 30, 'b' => 40}, 'a' => 10, 'b' => 20}) end end context "When looking up entries requiring evaluation" do let(:node) { Puppet::Node.new('localhost') } let(:compiler) { Puppet::Parser::Compiler.new(node)} let(:scope) { Puppet::Parser::Scope.new(compiler) } let(:parser) { Puppet::Pops::Parser::Parser.new() } it "should be possible to lookup a concatenated string" do scope['duck'] = 'Donald Fauntleroy Duck' expr = parser.parse_string('"Hello $duck"').current() bindings.bind.name('the_duck').to(expr) injector(lbinder).lookup(scope, 'the_duck').should == 'Hello Donald Fauntleroy Duck' end it "should be possible to post process lookup with a puppet lambda" do model = parser.parse_string('fake() |$value| {$value + 1 }').current bindings.bind.name('an_int').to(42).producer_options( :transformer => model.body.lambda) injector(lbinder).lookup(scope, 'an_int').should == 43 end it "should be possible to post process lookup with a ruby proc" do transformer = lambda {|scope, value| value + 1 } bindings.bind.name('an_int').to(42).producer_options( :transformer => transformer) injector(lbinder).lookup(scope, 'an_int').should == 43 end end end context "When there are problems with configuration" do let(:lbinder) { binder.new(layered_bindings) } it "reports error for surfacing abstract bindings" do bindings.bind.abstract.name('an_int') expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/The abstract binding .* was not overridden/) end it "does not report error for abstract binding that is ovrridden" do bindings.bind.abstract.name('an_int') bindings.bind.override.name('an_int').to(142) expect{ injector(lbinder).lookup(scope, 'an_int') }.to_not raise_error end it "reports error for overriding binding that does not override" do bindings.bind.override.name('an_int').to(42) expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding with unresolved 'override' detected/) end it "reports error for binding without producer" do bindings.bind.name('an_int') expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding without producer/) end end end \ No newline at end of file