diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index d40ef8c61..f9497a6c8 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,235 +1,228 @@ require 'puppet' require 'puppet/util/autoload' require 'prettyprint' require 'semver' # @api public class Puppet::Interface require 'puppet/interface/documentation' require 'puppet/interface/face_collection' require 'puppet/interface/action' require 'puppet/interface/action_builder' require 'puppet/interface/action_manager' require 'puppet/interface/option' require 'puppet/interface/option_builder' require 'puppet/interface/option_manager' include FullDocs include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager include Puppet::Interface::OptionManager extend Puppet::Interface::OptionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. - # @api private - # @deprecated - def autoloader - Puppet.deprecation_warning("Puppet::Interface.autoloader is deprecated; please use Puppet::Interface#loader instead") - @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/face") - end - # Lists all loaded faces # @return [Array] The names of the loaded faces def faces Puppet::Interface::FaceCollection.faces end # Register a face # @param instance [Puppet::Interface] The face # @return [void] # @api private def register(instance) Puppet::Interface::FaceCollection.register(instance) end # Defines a new Face. # @todo Talk about using Faces DSL inside the block # # @param name [Symbol] the name of the face # @param version [String] the version of the face (this should # conform to {http://semver.org/ Semantic Versioning}) # @overload define(name, version, {|| ... }) # @return [Puppet::Interface] The created face # @api public # @dsl Faces def define(name, version, &block) face = Puppet::Interface::FaceCollection[name, version] if face.nil? then face = self.new(name, version) Puppet::Interface::FaceCollection.register(face) # REVISIT: Shouldn't this be delayed until *after* we evaluate the # current block, not done before? --daniel 2011-04-07 face.load_actions end face.instance_eval(&block) if block_given? return face end # Retrieves a face by name and version. Use `:current` for the # version to get the most recent available version. # # @param name [Symbol] the name of the face # @param version [String, :current] the version of the face # # @return [Puppet::Interface] the face # # @api public def face?(name, version) Puppet::Interface::FaceCollection[name, version] end # Retrieves a face by name and version # # @param name [Symbol] the name of the face # @param version [String] the version of the face # # @return [Puppet::Interface] the face # # @api public def [](name, version) unless face = Puppet::Interface::FaceCollection[name, version] # REVISIT (#18042) no sense in rechecking if version == :current -- josh if Puppet::Interface::FaceCollection[name, :current] raise Puppet::Error, "Could not find version #{version} of #{name}" else raise Puppet::Error, "Could not find Puppet Face #{name.to_s}" end end face end # Retrieves an action for a face # @param name [Symbol] The face # @param action [Symbol] The action name # @param version [String, :current] The version of the face # @return [Puppet::Interface::Action] The action def find_action(name, action, version = :current) Puppet::Interface::FaceCollection.get_action_for_face(name, action, version) end end ######################################################################## # Documentation. We currently have to rewrite both getters because we share # the same instance between build-time and the runtime instance. When that # splits out this should merge into a module that both the action and face # include. --daniel 2011-04-17 # Returns the synopsis for the face. This shows basic usage and global # options. # @return [String] usage synopsis # @api private def synopsis build_synopsis self.name, '' end ######################################################################## # The name of the face # @return [Symbol] # @api private attr_reader :name # The version of the face # @return [SemVer] attr_reader :version # The autoloader instance for the face # @return [Puppet::Util::Autoload] # @api private attr_reader :loader private :loader # @api private def initialize(name, version, &block) unless SemVer.valid?(version) raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!" end @name = Puppet::Interface::FaceCollection.underscorize(name) @version = SemVer.new(version) # The few bits of documentation we actually demand. The default license # is a favour to our end users; if you happen to get that in a core face # report it as a bug, please. --daniel 2011-04-26 @authors = [] @license = 'All Rights Reserved' @loader = Puppet::Util::Autoload.new(@name, "puppet/face/#{@name}") instance_eval(&block) if block_given? end # Loads all actions defined in other files. # # @return [void] # @api private def load_actions loader.loadall end # Returns a string representation with the face's name and version # @return [String] def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end ######################################################################## # Action decoration, whee! You are not expected to care about this code, # which exists to support face building and construction. I marked these # private because the implementation is crude and ugly, and I don't yet know # enough to work out how to make it clean. # # Once we have established that these methods will likely change radically, # to be unrecognizable in the final outcome. At which point we will throw # all this away, replace it with something nice, and work out if we should # be making this visible to the outside world... --daniel 2011-04-14 private # @return [void] # @api private def __invoke_decorations(type, action, passed_args = [], passed_options = {}) [:before, :after].member?(type) or fail "unknown decoration type #{type}" # Collect the decoration methods matching our pass. methods = action.options.select do |name| passed_options.has_key? name end.map do |name| action.get_option(name).__decoration_name(type) end methods.reverse! if type == :after # Exceptions here should propagate up; this implements a hook we can use # reasonably for option validation. methods.each do |hook| respond_to? hook and self.__send__(hook, action, passed_args, passed_options) end end # @return [void] # @api private def __add_method(name, proc) meta_def(name, &proc) method(name).unbind end # @return [void] # @api private def self.__add_method(name, proc) define_method(name, proc) instance_method(name) end end diff --git a/spec/unit/interface_spec.rb b/spec/unit/interface_spec.rb index ca18a6b0c..50f902de0 100755 --- a/spec/unit/interface_spec.rb +++ b/spec/unit/interface_spec.rb @@ -1,271 +1,266 @@ require 'spec_helper' require 'puppet/face' require 'puppet/interface' describe Puppet::Interface do subject { Puppet::Interface } before :each do @faces = Puppet::Interface::FaceCollection. instance_variable_get("@faces").dup @dq = $".dup $".delete_if do |path| path =~ %r{/face/.*\.rb$} end Puppet::Interface::FaceCollection.instance_variable_get("@faces").clear end after :each do Puppet::Interface::FaceCollection.instance_variable_set("@faces", @faces) $".clear ; @dq.each do |item| $" << item end end describe "#[]" do it "should fail when no version is requested" do expect { subject[:huzzah] }.to raise_error ArgumentError end it "should raise an exception when the requested version is unavailable" do expect { subject[:huzzah, '17.0.0'] }.to raise_error(Puppet::Error, /Could not find version/) end it "should raise an exception when the requested face doesn't exist" do expect { subject[:burrble_toot, :current] }.to raise_error(Puppet::Error, /Could not find Puppet Face/) end describe "version matching" do { '1' => '1.1.1', '1.0' => '1.0.1', '1.0.1' => '1.0.1', '1.1' => '1.1.1', '1.1.1' => '1.1.1' }.each do |input, expect| it "should match #{input.inspect} to #{expect.inspect}" do face = subject[:version_matching, input] face.should be face.version.should == expect end end %w{1.0.2 1.2}.each do |input| it "should not match #{input.inspect} to any version" do expect { subject[:version_matching, input] }. to raise_error Puppet::Error, /Could not find version/ end end end end describe "#define" do it "should register the face" do face = subject.define(:face_test_register, '0.0.1') face.should == subject[:face_test_register, '0.0.1'] end it "should load actions" do subject.any_instance.expects(:load_actions) subject.define(:face_test_load_actions, '0.0.1') end it "should require a version number" do expect { subject.define(:no_version) }.to raise_error ArgumentError end it "should support summary builder and accessor methods" do subject.new(:foo, '1.0.0').should respond_to(:summary).with(0).arguments subject.new(:foo, '1.0.0').should respond_to(:summary=).with(1).arguments end # Required documentation methods... { :summary => "summary", :description => "This is the description of the stuff\n\nWhee", :examples => "This is my example", :short_description => "This is my custom short description", :notes => "These are my notes...", :author => "This is my authorship data", }.each do |attr, value| it "should support #{attr} in the builder" do face = subject.new(:builder, '1.0.0') do self.send(attr, value) end face.send(attr).should == value end end end describe "#initialize" do it "should require a version number" do expect { subject.new(:no_version) }.to raise_error ArgumentError end it "should require a valid version number" do expect { subject.new(:bad_version, 'Rasins') }. to raise_error ArgumentError end it "should instance-eval any provided block" do face = subject.new(:face_test_block, '0.0.1') do action(:something) do when_invoked {|_| "foo" } end end face.something.should == "foo" end end it "should have a name" do subject.new(:me, '0.0.1').name.should == :me end it "should stringify with its own name" do subject.new(:me, '0.0.1').to_s.should =~ /\bme\b/ end - # Why? - it "should create a class-level autoloader" do - subject.autoloader.should be_instance_of(Puppet::Util::Autoload) - end - it "should try to require faces that are not known" do subject::FaceCollection.expects(:load_face).with(:foo, :current) subject::FaceCollection.expects(:load_face).with(:foo, '0.0.1') expect { subject[:foo, '0.0.1'] }.to raise_error Puppet::Error end it_should_behave_like "things that declare options" do def add_options_to(&block) subject.new(:with_options, '0.0.1', &block) end end describe "with face-level display_global_options" do it "should not return any action level display_global_options" do face = subject.new(:with_display_global_options, '0.0.1') do display_global_options "environment" action :baz do when_invoked {|_| true } display_global_options "modulepath" end end face.display_global_options =~ ["environment"] end it "should not fail when a face d_g_o duplicates an action d_g_o" do expect { subject.new(:action_level_display_global_options, '0.0.1') do action :bar do when_invoked {|_| true } display_global_options "environment" end display_global_options "environment" end }.to_not raise_error end it "should work when two actions have the same d_g_o" do face = subject.new(:with_display_global_options, '0.0.1') do action :foo do when_invoked {|_| true} ; display_global_options "environment" end action :bar do when_invoked {|_| true} ; display_global_options "environment" end end face.get_action(:foo).display_global_options =~ ["environment"] face.get_action(:bar).display_global_options =~ ["environment"] end end describe "with inherited display_global_options" do end describe "with face-level options" do it "should not return any action-level options" do face = subject.new(:with_options, '0.0.1') do option "--foo" option "--bar" action :baz do when_invoked {|_| true } option "--quux" end end face.options.should =~ [:foo, :bar] end it "should fail when a face option duplicates an action option" do expect { subject.new(:action_level_options, '0.0.1') do action :bar do when_invoked {|_| true } option "--foo" end option "--foo" end }.to raise_error ArgumentError, /Option foo conflicts with existing option foo on/i end it "should work when two actions have the same option" do face = subject.new(:with_options, '0.0.1') do action :foo do when_invoked {|_| true } ; option "--quux" end action :bar do when_invoked {|_| true } ; option "--quux" end end face.get_action(:foo).options.should =~ [:quux] face.get_action(:bar).options.should =~ [:quux] end it "should only list options and not aliases" do face = subject.new(:face_options, '0.0.1') do option "--bar", "-b", "--foo-bar" end face.options.should =~ [:bar] end end describe "with inherited options" do let :parent do parent = Class.new(subject) parent.option("--inherited") parent.action(:parent_action) do when_invoked {|_| true } end parent end let :face do face = parent.new(:example, '0.2.1') face.option("--local") face.action(:face_action) do when_invoked {|_| true } end face end describe "#options" do it "should list inherited options" do face.options.should =~ [:inherited, :local] end it "should see all options on face actions" do face.get_action(:face_action).options.should =~ [:inherited, :local] end it "should see all options on inherited actions accessed on the subclass" do face.get_action(:parent_action).options.should =~ [:inherited, :local] end it "should not see subclass actions on the parent class" do parent.options.should =~ [:inherited] end it "should not see subclass actions on actions accessed on the parent class" do parent.get_action(:parent_action).options.should =~ [:inherited] end end describe "#get_option" do it "should return an inherited option object" do face.get_option(:inherited).should be_an_instance_of subject::Option end end end it_should_behave_like "documentation on faces" do subject do Puppet::Interface.new(:face_documentation, '0.0.1') end end end