diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index b7cb1169d..f6dac3911 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -1,415 +1,418 @@ require 'optparse' require 'puppet/util/plugins' # This class handles all the aspects of a Puppet application/executable # * setting up options # * setting up logs # * choosing what to run # * representing execution status # # === Usage # An application is a subclass of Puppet::Application. # # For legacy compatibility, # Puppet::Application[:example].run # is equivalent to # Puppet::Application::Example.new.run # # # class Puppet::Application::Example << Puppet::Application # # def preinit # # perform some pre initialization # @all = false # end # # # run_command is called to actually run the specified command # def run_command # send Puppet::Util::CommandLine.new.args.shift # end # # # option uses metaprogramming to create a method # # and also tells the option parser how to invoke that method # option("--arg ARGUMENT") do |v| # @args << v # end # # option("--debug", "-d") do |v| # @debug = v # end # # option("--all", "-a:) do |v| # @all = v # end # # def handle_unknown(opt,arg) # # last chance to manage an option # ... # # let's say to the framework we finally handle this option # true # end # # def read # # read action # end # # def write # # writeaction # end # # end # # === Preinit # The preinit block is the first code to be called in your application, before option parsing, # setup or command execution. # # === Options # Puppet::Application uses +OptionParser+ to manage the application options. # Options are defined with the +option+ method to which are passed various # arguments, including the long option, the short option, a description... # Refer to +OptionParser+ documentation for the exact format. # * If the option method is given a block, this one will be called whenever # the option is encountered in the command-line argument. # * If the option method has no block, a default functionnality will be used, that # stores the argument (or true/false if the option doesn't require an argument) in # the global (to the application) options array. # * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings: # * if +unknown+ was used with a block, it will be called with the option name and argument # * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for # a default behavior # # --help is managed directly by the Puppet::Application class, but can be overriden. # # === Setup # Applications can use the setup block to perform any initialization. # The defaul +setup+ behaviour is to: read Puppet configuration and manage log level and destination # # === What and how to run # If the +dispatch+ block is defined it is called. This block should return the name of the registered command # to be run. # If it doesn't exist, it defaults to execute the +main+ command if defined. # # === Execution state # The class attributes/methods of Puppet::Application serve as a global place to set and query the execution # status of the application: stopping, restarting, etc. The setting of the application status does not directly # aftect its running status; it's assumed that the various components within the application will consult these # settings appropriately and affect their own processing accordingly. Control operations (signal handlers and # the like) should set the status appropriately to indicate to the overall system that it's the process of # stopping or restarting (or just running as usual). # # So, if something in your application needs to stop the process, for some reason, you might consider: # # def stop_me! # # indicate that we're stopping # Puppet::Application.stop! # # ...do stuff... # end # # And, if you have some component that involves a long-running process, you might want to consider: # # def my_long_process(giant_list_to_munge) # giant_list_to_munge.collect do |member| # # bail if we're stopping # return if Puppet::Application.stop_requested? # process_member(member) # end # end module Puppet class Application require 'puppet/util' include Puppet::Util DOCPATTERN = File.expand_path(File.dirname(__FILE__) + "/util/command_line/*" ) class << self include Puppet::Util attr_accessor :run_status def clear! self.run_status = nil end def stop! self.run_status = :stop_requested end def restart! self.run_status = :restart_requested end # Indicates that Puppet::Application.restart! has been invoked and components should # do what is necessary to facilitate a restart. def restart_requested? :restart_requested == run_status end # Indicates that Puppet::Application.stop! has been invoked and components should do what is necessary # for a clean stop. def stop_requested? :stop_requested == run_status end # Indicates that one of stop! or start! was invoked on Puppet::Application, and some kind of process # shutdown/short-circuit may be necessary. def interrupted? [:restart_requested, :stop_requested].include? run_status end # Indicates that Puppet::Application believes that it's in usual running run_mode (no stop/restart request # currently active). def clear? run_status.nil? end # Only executes the given block if the run status of Puppet::Application is clear (no restarts, stops, # etc. requested). # Upon block execution, checks the run status again; if a restart has been requested during the block's # execution, then controlled_run will send a new HUP signal to the current process. # Thus, long-running background processes can potentially finish their work before a restart. def controlled_run(&block) return unless clear? result = block.call Process.kill(:HUP, $PID) if restart_requested? result end def should_parse_config @parse_config = true end def should_not_parse_config @parse_config = false end def should_parse_config? @parse_config = true if ! defined?(@parse_config) @parse_config end # used to declare code that handle an option def option(*options, &block) long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') fname = symbolize("handle_#{long}") if (block_given?) define_method(fname, &block) else define_method(fname) do |value| self.options["#{long}".to_sym] = value end end self.option_parser_commands << [options, fname] end def banner(banner = nil) @banner ||= banner end def option_parser_commands @option_parser_commands ||= ( superclass.respond_to?(:option_parser_commands) ? superclass.option_parser_commands.dup : [] ) @option_parser_commands end def find(name) klass = name.to_s.capitalize begin require File.join('puppet', 'application', name.to_s.downcase) rescue LoadError => e puts "Unable to find application '#{name}'. #{e}" Kernel::exit(1) end self.const_get(klass) end def [](name) find(name).new end # Sets or gets the run_mode name. Sets the run_mode name if a mode_name is # passed. Otherwise, gets the run_mode or a default run_mode # def run_mode( mode_name = nil) return @run_mode if @run_mode and not mode_name require 'puppet/util/run_mode' @run_mode = Puppet::Util::RunMode[ mode_name || :user ] end end attr_reader :options, :command_line # Every app responds to --version option("--version", "-V") do |arg| puts "#{Puppet.version}" exit end # Every app responds to --help option("--help", "-h") do |v| puts help exit end def should_parse_config? self.class.should_parse_config? end # override to execute code before running anything else def preinit end def initialize(command_line = nil) + require 'puppet/util/command_line' @command_line = command_line || Puppet::Util::CommandLine.new set_run_mode self.class.run_mode @options = {} require 'puppet' + require 'puppet/util/instrumentation' + Puppet::Util::Instrumentation.init end # WARNING: This is a totally scary, frightening, and nasty internal API. We # strongly advise that you do not use this, and if you insist, we will # politely allow you to keep both pieces of your broken code. # # We plan to provide a supported, long-term API to deliver this in a way # that you can use. Please make sure that you let us know if you do require # this, and this message is still present in the code. --daniel 2011-02-03 def set_run_mode(mode) @run_mode = mode $puppet_application_mode = @run_mode $puppet_application_name = name if Puppet.respond_to? :settings # This is to reduce the amount of confusion in rspec # because it might have loaded defaults.rb before the globals were set # and thus have the wrong defaults for the current application Puppet.settings.set_value(:confdir, Puppet.run_mode.conf_dir, :mutable_defaults) Puppet.settings.set_value(:vardir, Puppet.run_mode.var_dir, :mutable_defaults) Puppet.settings.set_value(:name, Puppet.application_name.to_s, :mutable_defaults) Puppet.settings.set_value(:logdir, Puppet.run_mode.logopts, :mutable_defaults) Puppet.settings.set_value(:rundir, Puppet.run_mode.run_dir, :mutable_defaults) Puppet.settings.set_value(:run_mode, Puppet.run_mode.name.to_s, :mutable_defaults) end end # This is the main application entry point def run exit_on_fail("initialize") { hook('preinit') { preinit } } exit_on_fail("parse options") { hook('parse_options') { parse_options } } exit_on_fail("parse configuration file") { Puppet.settings.parse } if should_parse_config? exit_on_fail("prepare for execution") { hook('setup') { setup } } exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes } exit_on_fail("run") { hook('run_command') { run_command } } end def main raise NotImplementedError, "No valid command or main" end def run_command main end def setup # Handle the logging settings if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] end def configure_indirector_routes route_file = Puppet[:route_file] if ::File.exists?(route_file) routes = YAML.load_file(route_file) application_routes = routes[name.to_s] Puppet::Indirector.configure_routes(application_routes) if application_routes end end def parse_options # Create an option parser option_parser = OptionParser.new(self.class.banner) # Add all global options to it. Puppet.settings.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| handlearg(option[0], arg) end end # Add options that are local to this application, which were # created using the "option()" metaprogramming method. If there # are any conflicts, this application's options will be favored. self.class.option_parser_commands.each do |options, fname| option_parser.on(*options) do |value| # Call the method that "option()" created. self.send(fname, value) end end # Scan command line. We just hand any exceptions to our upper levels, # rather than printing help and exiting, so that we can meaningfully # respond with context-sensitive help if we want to. --daniel 2011-04-12 option_parser.parse!(self.command_line.args) end def handlearg(opt, arg) # rewrite --[no-]option to --no-option if that's what was given if opt =~ /\[no-\]/ and !arg opt = opt.gsub(/\[no-\]/,'no-') end # otherwise remove the [no-] prefix to not confuse everybody opt = opt.gsub(/\[no-\]/, '') unless respond_to?(:handle_unknown) and send(:handle_unknown, opt, arg) # Puppet.settings.handlearg doesn't handle direct true/false :-) if arg.is_a?(FalseClass) arg = "false" elsif arg.is_a?(TrueClass) arg = "true" end Puppet.settings.handlearg(opt, arg) end end # this is used for testing def self.exit(code) exit(code) end def name self.class.to_s.sub(/.*::/,"").downcase.to_sym end def help "No help available for puppet #{name}" end private def exit_on_fail(message, code = 1) yield rescue ArgumentError, RuntimeError, NotImplementedError => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not #{message}: #{detail}" exit(code) end def hook(step,&block) Puppet::Plugins.send("before_application_#{step}",:application_object => self) x = yield Puppet::Plugins.send("after_application_#{step}",:application_object => self, :return_value => x) x end end end diff --git a/lib/puppet/application/instrumentation_data.rb b/lib/puppet/application/instrumentation_data.rb new file mode 100644 index 000000000..e4f86f196 --- /dev/null +++ b/lib/puppet/application/instrumentation_data.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_data < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/application/instrumentation_listener.rb b/lib/puppet/application/instrumentation_listener.rb new file mode 100644 index 000000000..64029b5c9 --- /dev/null +++ b/lib/puppet/application/instrumentation_listener.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_listener < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/application/instrumentation_probe.rb b/lib/puppet/application/instrumentation_probe.rb new file mode 100644 index 000000000..b31f95c45 --- /dev/null +++ b/lib/puppet/application/instrumentation_probe.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_probe < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/face/instrumentation_data.rb b/lib/puppet/face/instrumentation_data.rb new file mode 100644 index 000000000..1abf7cd00 --- /dev/null +++ b/lib/puppet/face/instrumentation_data.rb @@ -0,0 +1,28 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_data, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation listener accumulated data." + description <<-EOT + This subcommand allows to retrieve the various listener data. + EOT + + get_action(:destroy).summary "Invalid for this subcommand." + get_action(:save).summary "Invalid for this subcommand." + get_action(:search).summary "Invalid for this subcommand." + + find = get_action(:find) + find.summary "Retrieve listener data." + find.render_as = :pson + find.returns <<-EOT + The data of an instrumentation listener + EOT + find.examples <<-EOT + Retrieve listener data: + + $ puppet instrumentation_data find performance --terminus rest + EOT + +end diff --git a/lib/puppet/face/instrumentation_listener.rb b/lib/puppet/face/instrumentation_listener.rb new file mode 100644 index 000000000..de4a742a1 --- /dev/null +++ b/lib/puppet/face/instrumentation_listener.rb @@ -0,0 +1,96 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_listener, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation listeners." + description <<-EOT + This subcommand enables/disables or list instrumentation listeners. + EOT + + get_action(:destroy).summary "Invalid for this subcommand." + + find = get_action(:find) + find.summary "Retrieve a single listener." + find.render_as = :pson + find.returns <<-EOT + The status of an instrumentation listener + EOT + find.examples <<-EOT + Retrieve a given listener: + + $ puppet instrumentation_listener find performance --terminus rest + EOT + + search = get_action(:search) + search.summary "Retrieve all instrumentation listeners statuses." + search.arguments "" + search.render_as = :pson + search.returns <<-EOT + The statuses of all instrumentation listeners + EOT + search.short_description <<-EOT + This retrieves all instrumentation listeners + EOT + search.notes <<-EOT + Although this action always returns all instrumentation listeners, it requires a dummy search + key; this is a known bug. + EOT + search.examples <<-EOT + Retrieve the state of the listeners running in the remote puppet master: + + $ puppet instrumentation_listener search x --terminus rest + EOT + + def manage(name, activate) + Puppet::Util::Instrumentation::Listener.indirection.terminus_class = :rest + listener = Puppet::Face[:instrumentation_listener, '0.0.1'].find(name) + if listener + listener.enabled = activate + Puppet::Face[:instrumentation_listener, '0.0.1'].save(listener) + end + end + + action :enable do + summary "Enable a given instrumentation listener." + arguments "" + returns "Nothing." + description <<-EOT + Enable a given instrumentation listener. After being enabled the listener + will start receiving instrumentation notifications from the probes if those + are enabled. + EOT + examples <<-EOT + Enable the "performance" listener in the running master: + + $ puppet instrumentation_listener enable performance --terminus rest + EOT + + when_invoked do |name, options| + manage(name, true) + end + end + + action :disable do + summary "Disable a given instrumentation listener." + arguments "" + returns "Nothing." + description <<-EOT + Disable a given instrumentation listener. After being disabled the listener + will stop receiving instrumentation notifications from the probes. + EOT + examples <<-EOT + Disable the "performance" listener in the running master: + + $ puppet instrumentation_listener disable performance --terminus rest + EOT + + when_invoked do |name, options| + manage(name, false) + end + end + + get_action(:save).summary "API only: modify an instrumentation listener status." + get_action(:save).arguments "" +end diff --git a/lib/puppet/face/instrumentation_probe.rb b/lib/puppet/face/instrumentation_probe.rb new file mode 100644 index 000000000..52b331cb3 --- /dev/null +++ b/lib/puppet/face/instrumentation_probe.rb @@ -0,0 +1,77 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_probe, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation probes." + description <<-EOT + This subcommand enables/disables or list instrumentation listeners. + EOT + + get_action(:find).summary "Invalid for this subcommand." + + search = get_action(:search) + search.summary "Retrieve all probe statuses." + search.arguments "" + search.render_as = :pson + search.returns <<-EOT + The statuses of all instrumentation probes + EOT + search.short_description <<-EOT + This retrieves all instrumentation probes + EOT + search.notes <<-EOT + Although this action always returns all instrumentation probes, it requires a dummy search + key; this is a known bug. + EOT + search.examples <<-EOT + Retrieve the state of the probes running in the remote puppet master: + + $ puppet instrumentation_probe search x --terminus rest + EOT + + action :enable do + summary "Enable all instrumentation probes." + arguments "" + returns "Nothing." + description <<-EOT + Enable all instrumentation probes. After being enabled, all enabled listeners + will start receiving instrumentation notifications from the probes. + EOT + examples <<-EOT + Enable the probes for the running master: + + $ puppet instrumentation_probe enable x --terminus rest + EOT + + when_invoked do |name, options| + Puppet::Face[:instrumentation_probe, '0.0.1'].save(nil) + end + end + + action :disable do + summary "Disable all instrumentation probes." + arguments "" + returns "Nothing." + description <<-EOT + Disable all instrumentation probes. After being disabled, no listeners + will receive instrumentation notifications. + EOT + examples <<-EOT + Disable the probes for the running master: + + $ puppet instrumentation_probe disable x --terminus rest + EOT + + when_invoked do |name, options| + Puppet::Face[:instrumentation_probe, '0.0.1'].destroy(nil) + end + end + + get_action(:save).summary "API only: enable all instrumentation probes." + get_action(:save).arguments "" + + get_action(:destroy).summary "API only: disable all instrumentation probes." + get_action(:destroy).arguments "" +end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 20b260b83..7296be2b9 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,317 +1,324 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' +require 'puppet/util/instrumentation/instrumentable' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::Docs + extend Puppet::Util::Instrumentation::Instrumentable + + probe :find, :label => Proc.new { |parent, key, *args| "find_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :save, :label => Proc.new { |parent, key, *args| "save_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :search, :label => Proc.new { |parent, key, *args| "search_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :destroy, :label => Proc.new { |parent, key, *args| "destroy_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} @@indirections = [] # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@indirections.find { |i| i.name == name } end # Return a list of all known indirections. Used to generate the # reference. def self.instances @@indirections.collect { |i| i.name } end # Find an indirected model by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.model(name) return nil unless match = @@indirections.find { |i| i.name == name } match.model end attr_accessor :name, :model attr_reader :termini # Create and return our cache terminus. def cache raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class terminus(cache_class) end # Should we use a cache? def cache? cache_class ? true : false end attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) validate_terminus_class(class_name) if class_name @cache_class = class_name end # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) end # Set the time-to-live for instances created through this indirection. def ttl=(value) raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum) @ttl = value end # Default to the runinterval for the ttl. def ttl @ttl ||= Puppet[:runinterval].to_i end # Calculate the expiration date for a returned instance. def expiration Time.now + ttl end # Generate the full doc string. def doc text = "" text += scrub(@doc) + "\n\n" if @doc if s = terminus_setting text += "* **Terminus Setting**: #{terminus_setting}" end text end def initialize(model, name, options = {}) @model = model @name = name @termini = {} @cache_class = nil @terminus_class = nil raise(ArgumentError, "Indirection #{@name} is already defined") if @@indirections.find { |i| i.name == @name } @@indirections << self if mod = options[:extend] extend(mod) options.delete(:extend) end # This is currently only used for cache_class and terminus_class. options.each do |name, value| begin send(name.to_s + "=", value) rescue NoMethodError raise ArgumentError, "#{name} is not a valid Indirection parameter" end end end # Set up our request object. def request(*args) Puppet::Indirector::Request.new(self.name, *args) end # Return the singleton terminus for this indirection. def terminus(terminus_name = nil) # Get the name of the terminus. raise Puppet::DevError, "No terminus specified for #{self.name}; cannot redirect" unless terminus_name ||= terminus_class termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. attr_accessor :terminus_setting # Determine the terminus class. def terminus_class unless @terminus_class if setting = self.terminus_setting self.terminus_class = Puppet.settings[setting].to_sym else raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection #{self.name}" end end @terminus_class end def reset_terminus_class @terminus_class = nil end # Specify the terminus class to use. def terminus_class=(klass) validate_terminus_class(klass) @terminus_class = klass end # This is used by terminus_class= and cache=. def validate_terminus_class(terminus_class) raise ArgumentError, "Invalid terminus name #{terminus_class.inspect}" unless terminus_class and terminus_class.to_s != "" unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end end # Expire a cached object, if one is cached. Note that we don't actually # remove it, we expire it and write it back out to disk. This way people # can still use the expired object if they want. def expire(key, *args) request = request(:expire, key, *args) return nil unless cache? return nil unless instance = cache.find(request(:find, key, *args)) Puppet.info "Expiring the #{self.name} cache of #{instance.name}" # Set an expiration date in the past instance.expiration = Time.now - 60 cache.save(request(:save, instance, *args)) end # Search for an instance in the appropriate terminus, caching the # results if caching is configured.. def find(key, *args) request = request(:find, key, *args) terminus = prepare(request) if result = find_in_cache(request) return result end # Otherwise, return the result from the terminus, caching if appropriate. if ! request.ignore_terminus? and result = terminus.find(request) result.expiration ||= self.expiration if result.respond_to?(:expiration) if cache? and request.use_cache? Puppet.info "Caching #{self.name} for #{request.key}" cache.save request(:save, result, *args) end return terminus.respond_to?(:filter) ? terminus.filter(result) : result end nil end # Search for an instance in the appropriate terminus, and return a # boolean indicating whether the instance was found. def head(key, *args) request = request(:head, key, *args) terminus = prepare(request) # Look in the cache first, then in the terminus. Force the result # to be a boolean. !!(find_in_cache(request) || terminus.head(request)) end def find_in_cache(request) # See if our instance is in the cache and up to date. return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request) if cached.expired? Puppet.info "Not using expired #{self.name} for #{request.key} from cache; expired at #{cached.expiration}" return nil end Puppet.debug "Using cached #{self.name} for #{request.key}" cached rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Cached #{self.name} for #{request.key} failed: #{detail}" nil end # Remove something via the terminus. def destroy(key, *args) request = request(:destroy, key, *args) terminus = prepare(request) result = terminus.destroy(request) if cache? and cached = cache.find(request(:find, key, *args)) # Reuse the existing request, since it's equivalent. cache.destroy(request) end result end # Search for more than one instance. Should always return an array. def search(key, *args) request = request(:search, key, *args) terminus = prepare(request) if result = terminus.search(request) raise Puppet::DevError, "Search results from terminus #{terminus.name} are not an array" unless result.is_a?(Array) result.each do |instance| next unless instance.respond_to? :expiration instance.expiration ||= self.expiration end return result end end # Save the instance in the appropriate terminus. This method is # normally an instance method on the indirected class. def save(instance, key = nil) request = request(:save, key, instance) terminus = prepare(request) result = terminus.save(request) # If caching is enabled, save our document there cache.save(request) if cache? result end private # Check authorization if there's a hook available; fail if there is one # and it returns false. def check_authorization(request, terminus) # At this point, we're assuming authorization makes no sense without # client information. return unless request.node # This is only to authorize via a terminus-specific authorization hook. return unless terminus.respond_to?(:authorized?) unless terminus.authorized?(request) msg = "Not authorized to call #{request.method} on #{request}" msg += " with #{request.options.inspect}" unless request.options.empty? raise ArgumentError, msg end end # Setup a request, pick the appropriate terminus, check the request's authorization, and return it. def prepare(request) # Pick our terminus. if respond_to?(:select_terminus) unless terminus_name = select_terminus(request) raise ArgumentError, "Could not determine appropriate terminus for #{request}" end else terminus_name = terminus_class end dest_terminus = terminus(terminus_name) check_authorization(request, dest_terminus) dest_terminus end # Create a new terminus instance. def make_terminus(terminus_class) # Load our terminus class. unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end klass.new end end diff --git a/lib/puppet/indirector/instrumentation_data.rb b/lib/puppet/indirector/instrumentation_data.rb new file mode 100644 index 000000000..f1bea330d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationData +end diff --git a/lib/puppet/indirector/instrumentation_data/local.rb b/lib/puppet/indirector/instrumentation_data/local.rb new file mode 100644 index 000000000..6510d7f2d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/local.rb @@ -0,0 +1,19 @@ +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Local < Puppet::Indirector::Code + def find(request) + model.new(request.key) + end + + def search(request) + raise Puppet::DevError, "You cannot search for instrumentation data" + end + + def save(request) + raise Puppet::DevError, "You cannot save instrumentation data" + end + + def destroy(request) + raise Puppet::DevError, "You cannot remove instrumentation data" + end +end diff --git a/lib/puppet/indirector/instrumentation_data/rest.rb b/lib/puppet/indirector/instrumentation_data/rest.rb new file mode 100644 index 000000000..28b211781 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/instrumentation_listener.rb b/lib/puppet/indirector/instrumentation_listener.rb new file mode 100644 index 000000000..6aaa71ce6 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationListener +end diff --git a/lib/puppet/indirector/instrumentation_listener/local.rb b/lib/puppet/indirector/instrumentation_listener/local.rb new file mode 100644 index 000000000..72578014c --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/local.rb @@ -0,0 +1,23 @@ +require 'puppet/indirector/instrumentation_listener' + +class Puppet::Indirector::InstrumentationListener::Local < Puppet::Indirector::Code + def find(request) + Puppet::Util::Instrumentation[request.key] + end + + def search(request) + Puppet::Util::Instrumentation.listeners + end + + def save(request) + res = request.instance + Puppet::Util::Instrumentation[res.name] = res + nil # don't leak the listener + end + + def destroy(request) + listener = Puppet::Util::Instrumentation[request.key] + raise "Listener #{request.key} hasn't been subscribed" unless listener + Puppet::Util::Instrumentation.unsubscribe(listener) + end +end diff --git a/lib/puppet/indirector/instrumentation_listener/rest.rb b/lib/puppet/indirector/instrumentation_listener/rest.rb new file mode 100644 index 000000000..0bc8122ea --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/instrumentation_listener' +require 'puppet/indirector/rest' + +class Puppet::Indirector::InstrumentationListener::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/instrumentation_probe.rb b/lib/puppet/indirector/instrumentation_probe.rb new file mode 100644 index 000000000..3e514447a --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationProbe +end diff --git a/lib/puppet/indirector/instrumentation_probe/local.rb b/lib/puppet/indirector/instrumentation_probe/local.rb new file mode 100644 index 000000000..dd0a3f707 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe/local.rb @@ -0,0 +1,24 @@ +require 'puppet/indirector/instrumentation_probe' +require 'puppet/indirector/code' +require 'puppet/util/instrumentation/indirection_probe' + +class Puppet::Indirector::InstrumentationProbe::Local < Puppet::Indirector::Code + def find(request) + end + + def search(request) + probes = [] + Puppet::Util::Instrumentation::Instrumentable.each_probe do |probe| + probes << Puppet::Util::Instrumentation::IndirectionProbe.new("#{probe.klass}.#{probe.method}") + end + probes + end + + def save(request) + Puppet::Util::Instrumentation::Instrumentable.enable_probes + end + + def destroy(request) + Puppet::Util::Instrumentation::Instrumentable.disable_probes + end +end diff --git a/lib/puppet/indirector/instrumentation_probe/rest.rb b/lib/puppet/indirector/instrumentation_probe/rest.rb new file mode 100644 index 000000000..57e6fcf3d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' +require 'puppet/indirector/instrumentation_probe' + +class Puppet::Indirector::InstrumentationProbe::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/util/instrumentation.rb b/lib/puppet/util/instrumentation.rb new file mode 100644 index 000000000..bd0ed3ba5 --- /dev/null +++ b/lib/puppet/util/instrumentation.rb @@ -0,0 +1,173 @@ +require 'puppet' +require 'puppet/util/classgen' +require 'puppet/util/instance_loader' + +class Puppet::Util::Instrumentation + extend Puppet::Util::ClassGen + extend Puppet::Util::InstanceLoader + extend MonitorMixin + + # we're using a ruby lazy autoloader to prevent a loop when requiring listeners + # since this class sets up an indirection which is also used in Puppet::Indirector::Indirection + # which is used to setup indirections... + autoload :Listener, 'puppet/util/instrumentation/listener' + autoload :Data, 'puppet/util/instrumentation/data' + + # Set up autoloading and retrieving of instrumentation listeners. + instance_load :listener, 'puppet/util/instrumentation/listeners' + + class << self + attr_accessor :listeners, :listeners_of + end + + # instrumentation layer + + # Triggers an instrumentation + # + # Call this method around the instrumentation point + # Puppet::Util::Instrumentation.instrument(:my_long_computation) do + # ... a long computation + # end + # + # This will send an event to all the listeners of "my_long_computation". + # Note: this method uses ruby yield directive to call the instrumented code. + # It is usually way slower than calling start and stop directly around the instrumented code. + # For high traffic code path, it is thus advisable to not use this method. + def self.instrument(label, data = {}) + id = self.start(label, data) + yield + ensure + self.stop(label, id, data) + end + + # Triggers a "start" instrumentation event + # + # Important note: + # For proper use, the data hash instance used for start should also + # be used when calling stop. The idea is to use the current scope + # where start is called to retain a reference to 'data' so that it is possible + # to send it back to stop. + # This way listeners can match start and stop events more easily. + def self.start(label, data) + data[:started] = Time.now + publish(label, :start, data) + data[:id] = next_id + end + + # Triggers a "stop" instrumentation event + def self.stop(label, id, data) + data[:finished] = Time.now + publish(label, :stop, data) + end + + def self.publish(label, event, data) + each_listener(label) do |k,l| + l.notify(label, event, data) + end + end + + def self.listeners + @listeners.values + end + + def self.each_listener(label) + synchronize { + @listeners_of[label] ||= @listeners.select do |k,l| + l.listen_to?(label) + end + }.each do |l| + yield l + end + end + + # Adds a new listener + # + # Usage: + # Puppet::Util::Instrumentation.new_listener(:my_instrumentation, pattern) do + # + # def notify(label, data) + # ... do something for data... + # end + # end + # + # It is possible to use a "pattern". The listener will be notified only + # if the pattern match the label of the event. + # The pattern can be a symbol, a string or a regex. + # If no pattern is provided, then the listener will be called for every events + def self.new_listener(name, options = {}, &block) + Puppet.debug "new listener called #{name}" + name = symbolize(name) + listener = genclass(name, :hash => instance_hash(:listener), :block => block) + listener.send(:define_method, :name) do + name + end + subscribe(listener.new, options[:label_pattern], options[:event]) + end + + def self.subscribe(listener, label_pattern, event) + synchronize { + raise "Listener #{listener.name} is already subscribed" if @listeners.include?(listener.name) + Puppet.debug "registering instrumentation listener #{listener.name}" + @listeners[listener.name] = Listener.new(listener, label_pattern, event) + listener.subscribed if listener.respond_to?(:subscribed) + rehash + } + end + + def self.unsubscribe(listener) + synchronize { + Puppet.warning("#{listener.name} hasn't been registered but asked to be unregistered") unless @listeners.include?(listener.name) + Puppet.info "unregistering instrumentation listener #{listener.name}" + @listeners.delete(listener.name) + listener.unsubscribed if listener.respond_to?(:unsubscribed) + rehash + } + end + + def self.init + # let's init our probe indirection + require 'puppet/util/instrumentation/indirection_probe' + synchronize { + @listeners ||= {} + @listeners_of ||= {} + instance_loader(:listener).loadall + } + end + + def self.clear + synchronize { + @listeners = {} + @listeners_of = {} + @id = 0 + } + end + + def self.[](key) + synchronize { + key = symbolize(key) + @listeners[key] + } + end + + def self.[]=(key, value) + synchronize { + key = symbolize(key) + @listeners[key] = value + rehash + } + end + + private + + # should be called only under the guard + # self.synchronize + def self.rehash + @listeners_of = {} + end + + def self.next_id + synchronize { + @id = (@id || 0) + 1 + } + end +end diff --git a/lib/puppet/util/instrumentation/Instrumentable.rb b/lib/puppet/util/instrumentation/Instrumentable.rb new file mode 100644 index 000000000..5789dcbe2 --- /dev/null +++ b/lib/puppet/util/instrumentation/Instrumentable.rb @@ -0,0 +1,143 @@ +require 'monitor' +require 'puppet/util/instrumentation' + +# This is the central point of all declared probes. +# Every class needed to declare probes should include this module +# and declare the methods that are subject to instrumentation: +# +# class MyClass +# extend Puppet::Util::Instrumentation::Instrumentable +# +# probe :mymethod +# +# def mymethod +# ... this is code to be instrumented ... +# end +# end +module Puppet::Util::Instrumentation::Instrumentable + INSTRUMENTED_CLASSES = {}.extend(MonitorMixin) + + attr_reader :probes + + class Probe + attr_reader :klass, :method, :label, :data + + def initialize(method, klass, options = {}) + @method = method + @klass = klass + + @label = options[:label] || method + @data = options[:data] || {} + end + + def enable + raise "Probe already enabled" if enabled? + + # We're forced to perform this copy because in the class_eval'uated + # block below @method would be evaluated in the class context. It's better + # to close on locally-scoped variables than to resort to complex namespacing + # to get access to the probe instance variables. + method = @method; label = @label; data = @data + klass.class_eval { + alias_method("instrumented_#{method}", method) + define_method(method) do |*args| + id = nil + instrumentation_data = nil + begin + instrumentation_label = label.respond_to?(:call) ? label.call(self, args) : label + instrumentation_data = data.respond_to?(:call) ? data.call(self, args) : data + id = Puppet::Util::Instrumentation.start(instrumentation_label, instrumentation_data) + send("instrumented_#{method}".to_sym, *args) + ensure + Puppet::Util::Instrumentation.stop(instrumentation_label, id, instrumentation_data || {}) + end + end + } + @enabled = true + end + + def disable + raise "Probe is not enabled" unless enabled? + + # For the same reason as in #enable, we're forced to do a local + # copy + method = @method + klass.class_eval do + alias_method(method, "instrumented_#{method}") + remove_method("instrumented_#{method}".to_sym) + end + @enabled = false + end + + def enabled? + !!@enabled + end + end + + # Declares a new probe + # + # It is possible to pass several options that will be later on evaluated + # and sent to the instrumentation layer. + # + # label:: + # this can either be a static symbol/string or a block. If it's a block + # this one will be evaluated on every call of the instrumented method and + # should return a string or a symbol + # + # data:: + # this can be a hash or a block. If it's a block this one will be evaluated + # on every call of the instrumented method and should return a hash. + # + #Example: + # + # class MyClass + # extend Instrumentable + # + # probe :mymethod, :data => Proc.new { |args| { :data => args[1] } }, :label => Proc.new { |args| args[0] } + # + # def mymethod(name, options) + # end + # + # end + # + def probe(method, options = {}) + INSTRUMENTED_CLASSES.synchronize { + (@probes ||= []) << Probe.new(method, self, options) + INSTRUMENTED_CLASSES[self] = @probes + } + end + + def self.probes + @probes + end + + def self.probe_names + probe_names = [] + each_probe { |probe| probe_names << "#{probe.klass}.#{probe.method}" } + probe_names + end + + def self.enable_probes + each_probe { |probe| probe.enable } + end + + def self.disable_probes + each_probe { |probe| probe.disable } + end + + def self.clear_probes + INSTRUMENTED_CLASSES.synchronize { + INSTRUMENTED_CLASSES.clear + } + nil # do not leak our probes to the exterior world + end + + def self.each_probe + INSTRUMENTED_CLASSES.synchronize { + INSTRUMENTED_CLASSES.each_key do |klass| + klass.probes.each { |probe| yield probe } + end + } + nil # do not leak our probes to the exterior world + end +end \ No newline at end of file diff --git a/lib/puppet/util/instrumentation/data.rb b/lib/puppet/util/instrumentation/data.rb new file mode 100644 index 000000000..9157f58fc --- /dev/null +++ b/lib/puppet/util/instrumentation/data.rb @@ -0,0 +1,34 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' + +# This is just a transport class to be used through the instrumentation_data +# indirection. All the data resides in the real underlying listeners which this +# class delegates to. +class Puppet::Util::Instrumentation::Data + extend Puppet::Indirector + + indirects :instrumentation_data, :terminus_class => :local + + attr_reader :listener + + def initialize(listener_name) + @listener = Puppet::Util::Instrumentation[listener_name] + raise "Listener #{listener_name} wasn't registered" unless @listener + end + + def name + @listener.name + end + + def to_pson(*args) + result = { + 'document_type' => "Puppet::Util::Instrumentation::Data", + 'data' => { :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {}) + } + result.to_pson(*args) + end + + def self.from_pson(data) + data + end +end diff --git a/lib/puppet/util/instrumentation/indirection_probe.rb b/lib/puppet/util/instrumentation/indirection_probe.rb new file mode 100644 index 000000000..66e5f92ab --- /dev/null +++ b/lib/puppet/util/instrumentation/indirection_probe.rb @@ -0,0 +1,29 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' + +# We need to use a class other than Probe for the indirector because +# the Indirection class might declare some probes, and this would be a huge unbreakable +# dependency cycle. +class Puppet::Util::Instrumentation::IndirectionProbe + extend Puppet::Indirector + + indirects :instrumentation_probe, :terminus_class => :local + + attr_reader :probe_name + + def initialize(probe_name) + @probe_name = probe_name + end + + def to_pson(*args) + result = { + :document_type => "Puppet::Util::Instrumentation::IndirectionProbe", + :data => { :name => probe_name } + } + result.to_pson(*args) + end + + def self.from_pson(data) + self.new(data["name"]) + end +end \ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listener.rb b/lib/puppet/util/instrumentation/listener.rb new file mode 100644 index 000000000..42ec0c0e9 --- /dev/null +++ b/lib/puppet/util/instrumentation/listener.rb @@ -0,0 +1,60 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' + +class Puppet::Util::Instrumentation::Listener + include Puppet::Util + include Puppet::Util::Warnings + extend Puppet::Indirector + + indirects :instrumentation_listener, :terminus_class => :local + + attr_reader :pattern, :listener + attr_accessor :enabled + + def initialize(listener, pattern = nil, enabled = false) + @pattern = pattern.is_a?(Symbol) ? pattern.to_s : pattern + raise "Listener isn't a correct listener (it doesn't provide the notify method)" unless listener.respond_to?(:notify) + @listener = listener + @enabled = enabled + end + + def notify(label, event, data) + listener.notify(label, event, data) + rescue => e + warnonce("Error during instrumentation notification: #{e}") + end + + def listen_to?(label) + enabled? and (!@pattern || @pattern === label.to_s) + end + + def enabled? + !!@enabled + end + + def name + @listener.name.to_s + end + + def data + { :data => @listener.data } + end + + def to_pson(*args) + result = { + :document_type => "Puppet::Util::Instrumentation::Listener", + :data => { + :name => name, + :pattern => pattern, + :enabled => enabled? + } + } + result.to_pson(*args) + end + + def self.from_pson(data) + result = Puppet::Util::Instrumentation[data["name"]] + self.new(result.listener, result.pattern, data["enabled"]) + end +end diff --git a/lib/puppet/util/instrumentation/listeners/log.rb b/lib/puppet/util/instrumentation/listeners/log.rb new file mode 100644 index 000000000..59e9daff9 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/log.rb @@ -0,0 +1,29 @@ +require 'monitor' + +# This is an example instrumentation listener that stores the last +# 20 instrumented probe run time. +Puppet::Util::Instrumentation.new_listener(:log) do + + SIZE = 20 + + attr_accessor :last_logs + + def initialize + @last_logs = {}.extend(MonitorMixin) + end + + def notify(label, event, data) + return if event == :start + log_line = "#{label} took #{data[:finished] - data[:started]}" + @last_logs.synchronize { + (@last_logs[label] ||= []) << log_line + @last_logs[label].shift if @last_logs[label].length > SIZE + } + end + + def data + @last_logs.synchronize { + @last_logs.dup + } + end +end \ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listeners/performance.rb b/lib/puppet/util/instrumentation/listeners/performance.rb new file mode 100644 index 000000000..b3175a0a2 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/performance.rb @@ -0,0 +1,30 @@ +require 'monitor' + +Puppet::Util::Instrumentation.new_listener(:performance) do + + attr_reader :samples + + def initialize + @samples = {}.extend(MonitorMixin) + end + + def notify(label, event, data) + return if event == :start + + duration = data[:finished] - data[:started] + samples.synchronize do + @samples[label] ||= { :count => 0, :max => 0, :min => nil, :sum => 0, :average => 0 } + @samples[label][:count] += 1 + @samples[label][:sum] += duration + @samples[label][:max] = [ @samples[label][:max], duration ].max + @samples[label][:min] = [ @samples[label][:min], duration ].reject { |val| val.nil? }.min + @samples[label][:average] = @samples[label][:sum] / @samples[label][:count] + end + end + + def data + samples.synchronize do + @samples.dup + end + end +end \ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listeners/process_name.rb b/lib/puppet/util/instrumentation/listeners/process_name.rb new file mode 100644 index 000000000..88a185c41 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/process_name.rb @@ -0,0 +1,112 @@ +require 'monitor' + +# Unlike the other instrumentation plugins, this one doesn't give back +# data. Instead it changes the process name of the currently running process +# with the last labels and data. +Puppet::Util::Instrumentation.new_listener(:process_name) do + include Sync_m + + # start scrolling when process name is longer than + SCROLL_LENGTH = 50 + + attr_accessor :active, :reason + + def notify(label, event, data) + start(label) if event == :start + stop if event == :stop + end + + def start(activity) + push_activity(Thread.current, activity) + end + + def stop() + pop_activity(Thread.current) + end + + def subscribed + synchronize do + @oldname = $0 + @scroller ||= Thread.new do + loop do + scroll + sleep 1 + end + end + end + end + + def unsubscribed + synchronize do + $0 = @oldname if @oldname + Thread.kill(@scroller) + @scroller = nil + end + end + + def setproctitle + $0 = "#{base}: " + rotate(process_name,@x) + end + + def push_activity(thread, activity) + synchronize do + @reason ||= {} + @reason[thread] ||= [] + @reason[thread].push(activity) + setproctitle + end + end + + def pop_activity(thread) + synchronize do + @reason[thread].pop + if @reason[thread].empty? + @reason.delete(thread) + end + setproctitle + end + end + + def process_name + out = (@reason || {}).inject([]) do |out, reason| + out << "#{thread_id(reason[0])} #{reason[1].join(',')}" + end + out.join(' | ') + end + + # Getting the ruby thread id might not be portable to other ruby + # interpreters than MRI, because Thread#inspect might not return the same + # information on a different runtime. + def thread_id(thread) + thread.inspect.gsub(/^#<.*:0x([a-f0-9]+) .*>$/, '\1') + end + + def rotate(string, steps) + steps ||= 0 + if string.length > 0 && steps > 0 + steps = steps % string.length + return string[steps..-1].concat " -- #{string[0..(steps-1)]}" + end + string + end + + def base + basename = case Puppet.run_mode.name + when :master + "master" + when :agent + "agent" + else + "puppet" + end + end + + def scroll + @x ||= 1 + return if process_name.length < SCROLL_LENGTH + synchronize do + setproctitle + @x += 1 + end + end +end \ No newline at end of file diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index fd93ceb00..591358efb 100755 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -1,605 +1,611 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/application' require 'puppet' require 'getoptlong' describe Puppet::Application do before do + Puppet::Util::Instrumentation.stubs(:init) @app = Class.new(Puppet::Application).new @appclass = @app.class @app.stubs(:name).returns("test_app") # avoid actually trying to parse any settings Puppet.settings.stubs(:parse) end describe "finding" do before do @klass = Puppet::Application @klass.stubs(:puts) end it "should find classes in the namespace" do @klass.find("Agent").should == @klass::Agent end it "should not find classes outside the namespace", :'fails_on_ruby_1.9.2' => true do expect { @klass.find("String") }.to exit_with 1 end it "should exit if it can't find a class" do reg = "Unable to find application 'ThisShallNeverEverEverExist'. " reg += "no such file to load -- puppet/application/thisshallneverevereverexist" @klass.expects(:puts).with(reg) expect { @klass.find("ThisShallNeverEverEverExist") }.to exit_with 1 end end describe ".run_mode" do it "should default to user" do @appclass.run_mode.name.should == :user end it "should set and get a value" do @appclass.run_mode :agent @appclass.run_mode.name.should == :agent end end it "should sadly and frighteningly allow run_mode to change at runtime" do class TestApp < Puppet::Application run_mode :master def run_command # This is equivalent to calling these methods externally to the # instance, but since this is what "real world" code is likely to do # (and we need the class anyway) we may as well test that. --daniel 2011-02-03 set_run_mode self.class.run_mode "agent" end end Puppet[:run_mode].should == "user" expect { app = TestApp.new Puppet[:run_mode].should == "master" app.run app.class.run_mode.name.should == :agent $puppet_application_mode.name.should == :agent }.should_not raise_error Puppet[:run_mode].should == "agent" end it "it should not allow run mode to be set multiple times" do pending "great floods of tears, you can do this right now" # --daniel 2011-02-03 app = Puppet::Application.new expect { app.set_run_mode app.class.run_mode "master" $puppet_application_mode.name.should == :master app.set_run_mode app.class.run_mode "agent" $puppet_application_mode.name.should == :agent }.should raise_error end it "should explode when an invalid run mode is set at runtime, for great victory" # ...but you can, and while it will explode, that only happens too late for # us to easily test. --daniel 2011-02-03 it "should have a run entry-point" do @app.should respond_to(:run) end it "should have a read accessor to options" do @app.should respond_to(:options) end it "should include a default setup method" do @app.should respond_to(:setup) end it "should include a default preinit method" do @app.should respond_to(:preinit) end it "should include a default run_command method" do @app.should respond_to(:run_command) end it "should invoke main as the default" do @app.expects( :main ) @app.run_command end + it "should initialize the Puppet Instrumentation layer on creation" do + Puppet::Util::Instrumentation.expects(:init) + Class.new(Puppet::Application).new + end + describe 'when invoking clear!' do before :each do Puppet::Application.run_status = :stop_requested Puppet::Application.clear! end it 'should have nil run_status' do Puppet::Application.run_status.should be_nil end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return false for interrupted?' do Puppet::Application.interrupted?.should be_false end it 'should return true for clear?' do Puppet::Application.clear?.should be_true end end describe 'after invoking stop!' do before :each do Puppet::Application.run_status = nil Puppet::Application.stop! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :stop_requested' do Puppet::Application.run_status.should == :stop_requested end it 'should return true for stop_requested?' do Puppet::Application.stop_requested?.should be_true end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when invoking restart!' do before :each do Puppet::Application.run_status = nil Puppet::Application.restart! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :restart_requested' do Puppet::Application.run_status.should == :restart_requested end it 'should return true for restart_requested?' do Puppet::Application.restart_requested?.should be_true end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when performing a controlled_run' do it 'should not execute block if not :clear?' do Puppet::Application.run_status = :stop_requested target = mock 'target' target.expects(:some_method).never Puppet::Application.controlled_run do target.some_method end end it 'should execute block if :clear?' do Puppet::Application.run_status = nil target = mock 'target' target.expects(:some_method).once Puppet::Application.controlled_run do target.some_method end end describe 'on POSIX systems', :if => Puppet.features.posix? do it 'should signal process with HUP after block if restart requested during block execution', :'fails_on_ruby_1.9.2' => true do Puppet::Application.run_status = nil target = mock 'target' target.expects(:some_method).once old_handler = trap('HUP') { target.some_method } begin Puppet::Application.controlled_run do Puppet::Application.run_status = :restart_requested end ensure trap('HUP', old_handler) end end end after :each do Puppet::Application.run_status = nil end end describe "when parsing command-line options" do before :each do @app.command_line.stubs(:args).returns([]) Puppet.settings.stubs(:optparse_addargs).returns([]) end it "should pass the banner to the option parser" do option_parser = stub "option parser" option_parser.stubs(:on) option_parser.stubs(:parse!) @app.class.instance_eval do banner "banner" end OptionParser.expects(:new).with("banner").returns(option_parser) @app.parse_options end it "should get options from Puppet.settings.optparse_addargs" do Puppet.settings.expects(:optparse_addargs).returns([]) @app.parse_options end it "should add Puppet.settings options to OptionParser" do Puppet.settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option"]]) Puppet.settings.expects(:handlearg).with("--option", 'true') @app.command_line.stubs(:args).returns(["--option"]) @app.parse_options end it "should ask OptionParser to parse the command-line argument" do @app.command_line.stubs(:args).returns(%w{ fake args }) OptionParser.any_instance.expects(:parse!).with(%w{ fake args }) @app.parse_options end describe "when using --help" do it "should call exit" do @app.stubs(:puts) expect { @app.handle_help(nil) }.to exit_with 0 end end describe "when using --version" do it "should declare a version option" do @app.should respond_to(:handle_version) end it "should exit after printing the version" do @app.stubs(:puts) expect { @app.handle_version(nil) }.to exit_with 0 end end describe "when dealing with an argument not declared directly by the application" do it "should pass it to handle_unknown if this method exists" do Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled", :REQUIRED]]) @app.expects(:handle_unknown).with("--not-handled", "value").returns(true) @app.command_line.stubs(:args).returns(["--not-handled", "value"]) @app.parse_options end it "should pass it to Puppet.settings if handle_unknown says so" do Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]]) @app.stubs(:handle_unknown).with("--topuppet", "value").returns(false) Puppet.settings.expects(:handlearg).with("--topuppet", "value") @app.command_line.stubs(:args).returns(["--topuppet", "value"]) @app.parse_options end it "should pass it to Puppet.settings if there is no handle_unknown method" do Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]]) @app.stubs(:respond_to?).returns(false) Puppet.settings.expects(:handlearg).with("--topuppet", "value") @app.command_line.stubs(:args).returns(["--topuppet", "value"]) @app.parse_options end it "should transform boolean false value to string for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "false") @app.handlearg("--option", false) end it "should transform boolean true value to string for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "true") @app.handlearg("--option", true) end it "should transform boolean option to normal form for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "true") @app.handlearg("--[no-]option", true) end it "should transform boolean option to no- form for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--no-option", "false") @app.handlearg("--[no-]option", false) end end end describe "when calling default setup" do before :each do @app.stubs(:should_parse_config?).returns(false) @app.options.stubs(:[]) end [ :debug, :verbose ].each do |level| it "should honor option #{level}" do @app.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.stubs(:newdestination) @app.setup Puppet::Util::Log.level.should == (level == :verbose ? :info : :debug) end end it "should honor setdest option" do @app.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:newdestination).with(:syslog) @app.setup end end describe "when configuring routes" do include PuppetSpec::Files before :each do Puppet::Node.indirection.reset_terminus_class end after :each do Puppet::Node.indirection.reset_terminus_class end it "should use the routes specified for only the active application" do Puppet[:route_file] = tmpfile('routes') File.open(Puppet[:route_file], 'w') do |f| f.print <<-ROUTES test_app: node: terminus: exec other_app: node: terminus: plain catalog: terminus: invalid ROUTES end @app.configure_indirector_routes Puppet::Node.indirection.terminus_class.should == 'exec' end it "should not fail if the route file doesn't exist" do Puppet[:route_file] = "/dev/null/non-existent" expect { @app.configure_indirector_routes }.should_not raise_error end it "should raise an error if the routes file is invalid" do Puppet[:route_file] = tmpfile('routes') File.open(Puppet[:route_file], 'w') do |f| f.print <<-ROUTES invalid : : yaml ROUTES end expect { @app.configure_indirector_routes }.should raise_error end end describe "when running" do before :each do @app.stubs(:preinit) @app.stubs(:setup) @app.stubs(:parse_options) end it "should call preinit" do @app.stubs(:run_command) @app.expects(:preinit) @app.run end it "should call parse_options" do @app.stubs(:run_command) @app.expects(:parse_options) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should parse Puppet configuration if should_parse_config is called" do @app.stubs(:run_command) @app.class.should_parse_config Puppet.settings.expects(:parse) @app.run end it "should not parse_option if should_not_parse_config is called" do @app.stubs(:run_command) @app.class.should_not_parse_config Puppet.settings.expects(:parse).never @app.run end it "should parse Puppet configuration if needed" do @app.stubs(:run_command) @app.stubs(:should_parse_config?).returns(true) Puppet.settings.expects(:parse) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should call main as the default command" do @app.expects(:main) @app.run end it "should warn and exit if no command can be called" do $stderr.expects(:puts) expect { @app.run }.to exit_with 1 end it "should raise an error if dispatch returns no command" do @app.stubs(:get_command).returns(nil) $stderr.expects(:puts) expect { @app.run }.to exit_with 1 end it "should raise an error if dispatch returns an invalid command" do @app.stubs(:get_command).returns(:this_function_doesnt_exist) $stderr.expects(:puts) expect { @app.run }.to exit_with 1 end end describe "when metaprogramming" do describe "when calling option" do it "should create a new method named after the option" do @app.class.option("--test1","-t") do end @app.should respond_to(:handle_test1) end it "should transpose in option name any '-' into '_'" do @app.class.option("--test-dashes-again","-t") do end @app.should respond_to(:handle_test_dashes_again) end it "should create a new method called handle_test2 with option(\"--[no-]test2\")" do @app.class.option("--[no-]test2","-t") do end @app.should respond_to(:handle_test2) end describe "when a block is passed" do it "should create a new method with it" do @app.class.option("--[no-]test2","-t") do raise "I can't believe it, it works!" end lambda { @app.handle_test2 }.should raise_error end it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with { |*arg| arg[0] == "--[no-]test3" } @app.class.option("--[no-]test3","-t") do end @app.parse_options end it "should pass a block that calls our defined method" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with('--test4','-t').yields(nil) @app.expects(:send).with(:handle_test4, nil) @app.class.option("--test4","-t") do end @app.parse_options end end describe "when no block is given" do it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with("--test4","-t") @app.class.option("--test4","-t") @app.parse_options end it "should give to OptionParser a block that adds the the value to the options array" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with("--test4","-t").yields(nil) @app.options.expects(:[]=).with(:test4,nil) @app.class.option("--test4","-t") @app.parse_options end end end end end diff --git a/spec/unit/face/instrumentation_data.rb b/spec/unit/face/instrumentation_data.rb new file mode 100644 index 000000000..2d4cc74f6 --- /dev/null +++ b/spec/unit/face/instrumentation_data.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_data, '0.0.1'] do + it_should_behave_like "an indirector face" +end diff --git a/spec/unit/face/instrumentation_listener.rb b/spec/unit/face/instrumentation_listener.rb new file mode 100644 index 000000000..87f218855 --- /dev/null +++ b/spec/unit/face/instrumentation_listener.rb @@ -0,0 +1,38 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_listener, '0.0.1'] do + it_should_behave_like "an indirector face" + + [:enable, :disable].each do |m| + describe "when running ##{m}" do + before(:each) do + @listener = stub_everything 'listener' + Puppet::Face[:instrumentation_listener, '0.0.1'].stubs(:find).returns(@listener) + Puppet::Face[:instrumentation_listener, '0.0.1'].stubs(:save) + Puppet::Util::Instrumentation::Listener.indirection.stubs(:terminus_class=) + end + + it "should force the REST terminus" do + Puppet::Util::Instrumentation::Listener.indirection.expects(:terminus_class=).with(:rest) + subject.send(m, "dummy") + end + + it "should find the named listener" do + Puppet::Face[:instrumentation_listener, '0.0.1'].expects(:find).with("dummy").returns(@listener) + subject.send(m, "dummy") + end + + it "should #{m} the named listener" do + @listener.expects(:enabled=).with( m == :enable ) + subject.send(m, "dummy") + end + + it "should save finally the listener" do + Puppet::Face[:instrumentation_listener, '0.0.1'].expects(:save).with(@listener) + subject.send(m, "dummy") + end + end + end +end diff --git a/spec/unit/face/instrumentation_probe.rb b/spec/unit/face/instrumentation_probe.rb new file mode 100644 index 000000000..3e475906d --- /dev/null +++ b/spec/unit/face/instrumentation_probe.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_probe, '0.0.1'] do + it_should_behave_like "an indirector face" + + describe 'when running #enable' do + it 'should invoke #save' do + subject.expects(:save).with(nil) + subject.enable('hostname') + end + end + + describe 'when running #disable' do + it 'should invoke #destroy' do + subject.expects(:destroy).with(nil) + subject.disable('hostname') + end + end +end diff --git a/spec/unit/indirector/instrumentation_data/local_spec.rb b/spec/unit/indirector/instrumentation_data/local_spec.rb new file mode 100644 index 000000000..45ca1e07e --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/local_spec.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_data/local' + +describe Puppet::Indirector::InstrumentationData::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationData::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_data) + Puppet::Indirector::InstrumentationData::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationData::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationData::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @data = Puppet::Indirector::InstrumentationData::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding instrumentation data" do + it "should return a Instrumentation Data instance matching the key" do + end + end + + describe "when searching listeners" do + it "should raise an error" do + lambda { @data.search(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when saving listeners" do + it "should raise an error" do + lambda { @data.save(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when destroying listeners" do + it "should raise an error" do + lambda { @data.destroy(@reques) }.should raise_error(Puppet::DevError) + end + end +end diff --git a/spec/unit/indirector/instrumentation_data/rest_spec.rb b/spec/unit/indirector/instrumentation_data/rest_spec.rb new file mode 100644 index 000000000..762667ea7 --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/data' +require 'puppet/indirector/instrumentation_data/rest' + +describe Puppet::Indirector::InstrumentationData::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationData::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/instrumentation_listener/local_spec.rb b/spec/unit/indirector/instrumentation_listener/local_spec.rb new file mode 100644 index 000000000..b251736b4 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/local_spec.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/local' + +describe Puppet::Indirector::InstrumentationListener::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationListener::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_listener) + Puppet::Indirector::InstrumentationListener::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationListener::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationListener::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @listener = Puppet::Indirector::InstrumentationListener::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding listeners" do + it "should return a Instrumentation Listener instance matching the key" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instance) + @listener.find(@request).should == :instance + end + end + + describe "when searching listeners" do + it "should return a list of all loaded Instrumentation Listenesrs irregardless of the given key" do + Puppet::Util::Instrumentation.expects(:listeners).returns([:instance1, :instance2]) + @listener.search(@request).should == [:instance1, :instance2] + end + end + + describe "when saving listeners" do + it "should set the new listener to the global listener list" do + newlistener = stub 'listener', :name => @name + @request.stubs(:instance).returns(newlistener) + Puppet::Util::Instrumentation.expects(:[]=).with("me", newlistener) + @listener.save(@request) + end + end + + describe "when destroying listeners" do + it "should raise an error if listener wasn't subscribed" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(nil) + lambda { @listener.destroy(@request) }.should raise_error + end + + it "should unsubscribe the listener" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instancce) + Puppet::Util::Instrumentation.expects(:unsubscribe).with(:instancce) + @listener.destroy(@request) + end + end +end diff --git a/spec/unit/indirector/instrumentation_listener/rest_spec.rb b/spec/unit/indirector/instrumentation_listener/rest_spec.rb new file mode 100644 index 000000000..6355a1c53 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/rest' + +describe Puppet::Indirector::InstrumentationListener::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationListener::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/instrumentation_probe/local_spec.rb b/spec/unit/indirector/instrumentation_probe/local_spec.rb new file mode 100644 index 000000000..187752f7a --- /dev/null +++ b/spec/unit/indirector/instrumentation_probe/local_spec.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/indirection_probe' +require 'puppet/indirector/instrumentation_probe/local' +require 'puppet/util/instrumentation/instrumentable' + +describe Puppet::Indirector::InstrumentationProbe::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationProbe::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_probe) + Puppet::Indirector::InstrumentationProbe::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationProbe::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationProbe::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @probe = Puppet::Indirector::InstrumentationProbe::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding probes" do + it "should do nothing" do + @probe.find(@request).should be_nil + end + end + + describe "when searching probes" do + it "should return a list of all loaded probes irregardless of the given key" do + instance1 = stub 'instance1', :method => "probe1", :klass => "Klass1" + instance2 = stub 'instance2', :method => "probe2", :klass => "Klass2" + Puppet::Util::Instrumentation::IndirectionProbe.expects(:new).with("Klass1.probe1").returns(:instance1) + Puppet::Util::Instrumentation::IndirectionProbe.expects(:new).with("Klass2.probe2").returns(:instance2) + Puppet::Util::Instrumentation::Instrumentable.expects(:each_probe).multiple_yields(instance1, instance2) + @probe.search(@request).should == [ :instance1, :instance2] + end + end + + describe "when saving probes" do + it "should enable probes" do + newprobe = stub 'probe', :name => @name + @request.stubs(:instance).returns(newprobe) + Puppet::Util::Instrumentation::Instrumentable.expects(:enable_probes) + @probe.save(@request) + end + end + + describe "when destroying probes" do + it "should disable probes" do + newprobe = stub 'probe', :name => @name + @request.stubs(:instance).returns(newprobe) + Puppet::Util::Instrumentation::Instrumentable.expects(:disable_probes) + @probe.destroy(@request) + end + end +end diff --git a/spec/unit/indirector/instrumentation_probe/rest_spec.rb b/spec/unit/indirector/instrumentation_probe/rest_spec.rb new file mode 100644 index 000000000..0b73fbdf5 --- /dev/null +++ b/spec/unit/indirector/instrumentation_probe/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/indirection_probe' +require 'puppet/indirector/instrumentation_probe/rest' + +describe Puppet::Indirector::InstrumentationProbe::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationProbe::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/util/instrumentation/data_spec.rb b/spec/unit/util/instrumentation/data_spec.rb new file mode 100755 index 000000000..c2465f622 --- /dev/null +++ b/spec/unit/util/instrumentation/data_spec.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' + +describe Puppet::Util::Instrumentation::Data do + Puppet::Util::Instrumentation::Data + + before(:each) do + @listener = stub 'listener', :name => "name" + Puppet::Util::Instrumentation.stubs(:[]).with("name").returns(@listener) + end + + it "should indirect instrumentation_data" do + Puppet::Util::Instrumentation::Data.indirection.name.should == :instrumentation_data + end + + it "should lookup the corresponding listener" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(@listener) + Puppet::Util::Instrumentation::Data.new("name") + end + + it "should error if the listener can not be found" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(nil) + expect { Puppet::Util::Instrumentation::Data.new("name") }.to raise_error + end + + it "should return pson data" do + data = Puppet::Util::Instrumentation::Data.new("name") + @listener.stubs(:data).returns({ :this_is_data => "here also" }) + data.should set_json_attribute('name').to("name") + data.should set_json_attribute('this_is_data').to("here also") + end + + it "should not error if the underlying listener doesn't have data" do + lambda { Puppet::Util::Instrumentation::Data.new("name").to_pson }.should_not raise_error + end + + it "should return a hash containing data when unserializing from pson" do + Puppet::Util::Instrumentation::Data.from_pson({:name => "name"}).should == {:name => "name"} + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/indirection_probe_spec.rb b/spec/unit/util/instrumentation/indirection_probe_spec.rb new file mode 100644 index 000000000..654825c9a --- /dev/null +++ b/spec/unit/util/instrumentation/indirection_probe_spec.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/indirection_probe' + +describe Puppet::Util::Instrumentation::IndirectionProbe do + Puppet::Util::Instrumentation::IndirectionProbe + + it "should indirect instrumentation_probe" do + Puppet::Util::Instrumentation::IndirectionProbe.indirection.name.should == :instrumentation_probe + end + + it "should return pson data" do + probe = Puppet::Util::Instrumentation::IndirectionProbe.new("probe") + probe.should set_json_attribute('name').to("probe") + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/instrumentable_spec.rb b/spec/unit/util/instrumentation/instrumentable_spec.rb new file mode 100755 index 000000000..dd2ad3084 --- /dev/null +++ b/spec/unit/util/instrumentation/instrumentable_spec.rb @@ -0,0 +1,186 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/instrumentable' + +describe Puppet::Util::Instrumentation::Instrumentable::Probe do + + before(:each) do + Puppet::Util::Instrumentation.stubs(:start) + Puppet::Util::Instrumentation.stubs(:stop) + + class ProbeTest + def mymethod(arg1, arg2, arg3) + :it_worked + end + end + end + + after(:each) do + if ProbeTest.method_defined?(:instrumented_mymethod) + ProbeTest.class_eval { + remove_method(:mymethod) + alias_method(:mymethod, :instrumented_mymethod) + } + end + Puppet::Util::Instrumentation::Instrumentable.clear_probes + end + + describe "when enabling a probe" do + it "should raise an error if the probe is already enabled" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + lambda { probe.enable }.should raise_error + end + + it "should rename the original method name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + ProbeTest.new.should respond_to(:instrumented_mymethod) + end + + it "should create a new method of the original name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + ProbeTest.new.should respond_to(:mymethod) + end + end + + describe "when disabling a probe" do + it "should raise an error if the probe is already enabled" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + lambda { probe.disable }.should raise_error + end + + it "should rename the original method name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + probe.disable + + Puppet::Util::Instrumentation.expects(:start).never + Puppet::Util::Instrumentation.expects(:stop).never + ProbeTest.new.mymethod(1,2,3).should == :it_worked + end + + it "should remove the created method" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + probe.disable + ProbeTest.new.should_not respond_to(:instrumented_mymethod) + end + end + + describe "when a probe is called" do + it "should call the original method" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3) + test.mymethod(1,2,3) + end + + it "should start the instrumentation" do + Puppet::Util::Instrumentation.expects(:start) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.mymethod(1,2,3) + end + + it "should stop the instrumentation" do + Puppet::Util::Instrumentation.expects(:stop) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.mymethod(1,2,3) + end + + describe "and the original method raises an exception" do + it "should propagate the exception" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3).raises + lambda { test.mymethod(1,2,3) }.should raise_error + end + + it "should stop the instrumentation" do + Puppet::Util::Instrumentation.expects(:stop) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3).raises + lambda { test.mymethod(1,2,3) }.should raise_error + end + end + + describe "with a static label" do + it "should send the label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => :mylabel) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == :mylabel }.returns(42) + Puppet::Util::Instrumentation.expects(:stop).with(:mylabel, 42, {}) + test.mymethod(1,2,3) + end + end + + describe "with a dynamic label" do + it "should send the evaluated label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => Proc.new { |parent,args| "dynamic#{args[0]}" } ) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == "dynamic1" }.returns(42) + Puppet::Util::Instrumentation.expects(:stop).with("dynamic1",42,{}) + test.mymethod(1,2,3) + end + end + + describe "with static data" do + it "should send the data to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => { :static_data => "nothing" }) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :static_data => "nothing" }} + test.mymethod(1,2,3) + end + end + + describe "with dynamic data" do + it "should send the evaluated label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => Proc.new { |parent, args| { :key => args[0] } } ) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :key => 1 } } + Puppet::Util::Instrumentation.expects(:stop) + test.mymethod(1,2,3) + end + end + end +end + +describe Puppet::Util::Instrumentation::Instrumentable do + before(:each) do + class ProbeTest2 + extend Puppet::Util::Instrumentation::Instrumentable + probe :mymethod + def mymethod(arg1,arg2,arg3) + end + end + end + + after do + Puppet::Util::Instrumentation::Instrumentable.clear_probes + end + + it "should allow probe definition" do + Puppet::Util::Instrumentation::Instrumentable.probe_names.should be_include("ProbeTest2.mymethod") + end + + it "should be able to enable all probes" do + Puppet::Util::Instrumentation::Instrumentable.enable_probes + ProbeTest2.new.should respond_to(:instrumented_mymethod) + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/listener_spec.rb b/spec/unit/util/instrumentation/listener_spec.rb new file mode 100755 index 000000000..bc49d265c --- /dev/null +++ b/spec/unit/util/instrumentation/listener_spec.rb @@ -0,0 +1,100 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' + +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/listener' + +describe Puppet::Util::Instrumentation::Listener do + + Listener = Puppet::Util::Instrumentation::Listener + + before(:each) do + @delegate = stub 'listener', :notify => nil, :name => 'listener' + @listener = Listener.new(@delegate) + @listener.enabled = true + end + + it "should indirect instrumentation_listener" do + Listener.indirection.name.should == :instrumentation_listener + end + + it "should raise an error if delegate doesn't support notify" do + lambda { Listener.new(Object.new) }.should raise_error + end + + it "should not be enabled by default" do + Listener.new(@delegate).should_not be_enabled + end + + it "should delegate notification" do + @delegate.expects(:notify).with(:event, :start, {}) + listener = Listener.new(@delegate) + listener.notify(:event, :start, {}) + end + + it "should not listen is not enabled" do + @listener.enabled = false + @listener.should_not be_listen_to(:label) + end + + it "should listen to all label if created without pattern" do + @listener.should be_listen_to(:improbable_label) + end + + it "should listen to specific string pattern" do + listener = Listener.new(@delegate, "specific") + listener.enabled = true + listener.should be_listen_to(:specific) + end + + it "should not listen to non-matching string pattern" do + listener = Listener.new(@delegate, "specific") + listener.enabled = true + listener.should_not be_listen_to(:unspecific) + end + + it "should listen to specific regex pattern" do + listener = Listener.new(@delegate, /spe.*/) + listener.enabled = true + listener.should be_listen_to(:specific_pattern) + end + + it "should not listen to non matching regex pattern" do + listener = Listener.new(@delegate, /^match.*/) + listener.enabled = true + listener.should_not be_listen_to(:not_matching) + end + + it "should delegate its name to the underlying listener" do + @delegate.expects(:name).returns("myname") + @listener.name.should == "myname" + end + + it "should delegate data fetching to the underlying listener" do + @delegate.expects(:data).returns(:data) + @listener.data.should == {:data => :data } + end + + describe "when serializing to pson" do + it "should return a pson object containing pattern, name and status" do + @listener.should set_json_attribute('enabled').to(true) + @listener.should set_json_attribute('name').to("listener") + end + end + + describe "when deserializing from pson" do + it "should lookup the archetype listener from the instrumentation layer" do + Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener) + Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener"}) + end + + it "should create a new listener shell instance delegating to the archetypal listener" do + Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener) + @listener.stubs(:listener).returns(@delegate) + Puppet::Util::Instrumentation::Listener.expects(:new).with(@delegate, nil, true) + Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener", "enabled" => true}) + end + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/log_spec.rb b/spec/unit/util/instrumentation/listeners/log_spec.rb new file mode 100755 index 000000000..8359625a1 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/log_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +log = Puppet::Util::Instrumentation.listener(:log) + +describe log do + before(:each) do + @log = log.new + end + + it "should have a notify method" do + @log.should respond_to(:notify) + end + + it "should have a data method" do + @log.should respond_to(:data) + end + + it "should keep data for stop event" do + @log.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @log.data.should == {:test=>["test took 1.0"]} + end + + it "should not keep data for start event" do + @log.notify(:test, :start, { :started => Time.at(123456789)}) + @log.data.should be_empty + end + + it "should not keep more than 20 events per label" do + 25.times { @log.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) } + @log.data[:test].size.should == 20 + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/performance_spec.rb b/spec/unit/util/instrumentation/listeners/performance_spec.rb new file mode 100755 index 000000000..4cecbe3f6 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/performance_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +performance = Puppet::Util::Instrumentation.listener(:performance) + +describe performance do + before(:each) do + @performance = performance.new + end + + it "should have a notify method" do + @performance.should respond_to(:notify) + end + + it "should have a data method" do + @performance.should respond_to(:data) + end + + it "should keep data for stop event" do + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @performance.data.should == {:test=>{:average=>1.0, :count=>1, :min=>1.0, :max=>1.0, :sum=>1.0}} + end + + it "should accumulate performance statistics" do + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456791)}) + + @performance.data.should == {:test=>{:average=>1.5, :count=>2, :min=>1.0, :max=>2.0, :sum=>3.0}} + end + + it "should not keep data for start event" do + @performance.notify(:test, :start, { :started => Time.at(123456789)}) + @performance.data.should be_empty + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/process_name_spec.rb b/spec/unit/util/instrumentation/listeners/process_name_spec.rb new file mode 100755 index 000000000..a67dcb440 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/process_name_spec.rb @@ -0,0 +1,200 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +process_name = Puppet::Util::Instrumentation.listener(:process_name) + +describe process_name do + before(:each) do + @process_name = process_name.new + end + + it "should have a notify method" do + @process_name.should respond_to(:notify) + end + + it "should not have a data method" do + @process_name.should_not respond_to(:data) + end + + describe "when managing thread activity" do + before(:each) do + @process_name.stubs(:setproctitle) + @process_name.stubs(:base).returns("base") + end + + it "should be able to append activity" do + thread1 = stub 'thread1' + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread1,"activity2") + + @process_name.reason[:thread1].should == ["activity1", "activity2"] + end + + it "should be able to remove activity" do + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread1,"activity1") + @process_name.pop_activity(:thread1) + + @process_name.reason[:thread1].should == ["activity1"] + end + + it "should maintain activity thread by thread" do + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread2,"activity2") + + @process_name.reason[:thread1].should == ["activity1"] + @process_name.reason[:thread2].should == ["activity2"] + end + + it "should set process title" do + @process_name.expects(:setproctitle) + + @process_name.push_activity("thread1","activity1") + end + end + + describe "when computing the current process name" do + before(:each) do + @process_name.stubs(:setproctitle) + @process_name.stubs(:base).returns("base") + end + + it "should include every running thread activity" do + thread1 = stub 'thread1', :inspect => "\#", :hash => 1 + thread2 = stub 'thread2', :inspect => "\#", :hash => 0 + + @process_name.push_activity(thread1,"Compiling node1.domain.com") + @process_name.push_activity(thread2,"Compiling node4.domain.com") + @process_name.push_activity(thread1,"Parsing file site.pp") + @process_name.push_activity(thread2,"Parsing file node.pp") + + @process_name.process_name.should == "12344321 Compiling node4.domain.com,Parsing file node.pp | deadbeef Compiling node1.domain.com,Parsing file site.pp" + end + end + + describe "when finding base process name" do + {:master => "master", :agent => "agent", :user => "puppet"}.each do |program,base| + it "should return #{base} for #{program}" do + Puppet.run_mode.stubs(:name).returns(program) + @process_name.base.should == base + end + end + end + + describe "when finding a thread id" do + it "should return the id from the thread inspect string" do + thread = stub 'thread', :inspect => "\#" + @process_name.thread_id(thread).should == "1234abdc" + end + end + + describe "when scrolling the instrumentation string" do + it "should rotate the string of various step" do + @process_name.rotate("this is a rotation", 10).should == "rotation -- this is a " + end + + it "should not rotate the string for the 0 offset" do + @process_name.rotate("this is a rotation", 0).should == "this is a rotation" + end + end + + describe "when setting process name" do + before(:each) do + @process_name.stubs(:process_name).returns("12345 activity") + @process_name.stubs(:base).returns("base") + @oldname = $0 + end + + after(:each) do + $0 = @oldname + end + + it "should do it if the feature is enabled" do + @process_name.setproctitle + + $0.should == "base: 12345 activity" + end + end + + describe "when subscribed" do + before(:each) do + thread = stub 'thread', :inspect => "\#" + Thread.stubs(:current).returns(thread) + end + + it "should start the scroller" do + Thread.expects(:new) + @process_name.subscribed + end + end + + describe "when unsubscribed" do + before(:each) do + @thread = stub 'scroller', :inspect => "\#" + Thread.stubs(:new).returns(@thread) + Thread.stubs(:kill) + @oldname = $0 + @process_name.subscribed + end + + after(:each) do + $0 = @oldname + end + + it "should stop the scroller" do + Thread.expects(:kill).with(@thread) + @process_name.unsubscribed + end + + it "should reset the process name" do + $0 = "let's see what happens" + @process_name.unsubscribed + $0.should == @oldname + end + end + + describe "when setting a probe" do + before(:each) do + thread = stub 'thread', :inspect => "\#" + Thread.stubs(:current).returns(thread) + Thread.stubs(:new) + @process_name.active = true + end + + it "should push current thread activity and execute the block" do + @process_name.notify(:instrumentation, :start, {}) + $0.should == "puppet: 1234abdc instrumentation" + @process_name.notify(:instrumentation, :stop, {}) + end + + it "should finally pop the activity" do + @process_name.notify(:instrumentation, :start, {}) + @process_name.notify(:instrumentation, :stop, {}) + $0.should == "puppet: " + end + end + + describe "when scrolling" do + it "should do nothing for shorter process names" do + @process_name.expects(:setproctitle).never + @process_name.scroll + end + + it "should call setproctitle" do + @process_name.stubs(:process_name).returns("x" * 60) + @process_name.expects(:setproctitle) + @process_name.scroll + end + + it "should increment rotation offset" do + name = "x" * 60 + @process_name.stubs(:process_name).returns(name) + @process_name.expects(:rotate).once.with(name,1).returns("") + @process_name.expects(:rotate).once.with(name,2).returns("") + @process_name.scroll + @process_name.scroll + end + end +end \ No newline at end of file diff --git a/spec/unit/util/instrumentation_spec.rb b/spec/unit/util/instrumentation_spec.rb new file mode 100755 index 000000000..0d19ee03c --- /dev/null +++ b/spec/unit/util/instrumentation_spec.rb @@ -0,0 +1,181 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +require 'puppet/util/instrumentation' + +describe Puppet::Util::Instrumentation do + + Instrumentation = Puppet::Util::Instrumentation + + after(:each) do + Instrumentation.clear + end + + it "should instance-load instrumentation listeners" do + Instrumentation.instance_loader(:listener).should be_instance_of(Puppet::Util::Autoload) + end + + it "should have a method for registering instrumentation listeners" do + Instrumentation.should respond_to(:new_listener) + end + + it "should have a method for retrieving instrumentation listener by name" do + Instrumentation.should respond_to(:listener) + end + + describe "when registering listeners" do + it "should evaluate the supplied block as code for a class" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + end + + it "should subscribe a new listener instance" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + Instrumentation.listeners.size.should == 1 + Instrumentation.listeners[0].pattern.should == "for_this_label" + end + + it "should be possible to access listeners by name" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + Instrumentation["testing"].should_not be_nil + end + + it "should be possible to store a new listener by name" do + listener = stub 'listener' + Instrumentation["testing"] = listener + Instrumentation["testing"].should == listener + end + + it "should fail if listener is already subscribed" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + expect { Instrumentation.subscribe(listener, :for_this_label, :all) }.to raise_error + end + + it 'should call #unsubscribed' do + listener = stub 'listener', :notify => nil, :name => "mylistener" + + listener.expects(:subscribed) + + Instrumentation.subscribe(listener, :for_this_label, :all) + end + end + + describe "when unsubscribing listener" do + it "should remove it from the listeners" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.unsubscribe(listener) + Instrumentation.listeners.size.should == 0 + end + + it "should warn if the listener wasn't subscribed" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Puppet.expects(:warning) + Instrumentation.unsubscribe(listener) + end + + it 'should call #unsubscribed' do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + + listener.expects(:unsubscribed) + + Instrumentation.unsubscribe(listener) + end + end + + describe "when firing events" do + it "should be able to find all listeners matching a label" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + + count = 0 + Instrumentation.each_listener(:for_this_label) { |l| count += 1 } + count.should == 1 + end + + it "should fire events to matching listeners" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + + listener.expects(:notify).with(:for_this_label, :start, {}) + + Instrumentation.publish(:for_this_label, :start, {}) + end + + it "should not fire events to non-matching listeners" do + listener1 = stub 'listener1', :notify => nil, :name => "mylistener1" + listener2 = stub 'listener2', :notify => nil, :name => "mylistener2" + Instrumentation.subscribe(listener1, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + Instrumentation.subscribe(listener2, :for_this_other_label, :all) + Instrumentation.listeners[1].enabled = true + + listener1.expects(:notify).never + listener2.expects(:notify).with(:for_this_other_label, :start, {}) + + Instrumentation.publish(:for_this_other_label, :start, {}) + end + end + + describe "when instrumenting code" do + before(:each) do + Instrumentation.stubs(:publish) + end + describe "with a block" do + it "should execute it" do + executed = false + Instrumentation.instrument(:event) do + executed = true + end + executed.should be_true + end + + it "should publish an event before execution" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :start } + Instrumentation.instrument(:event) {} + end + + it "should publish an event after execution" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :stop } + Instrumentation.instrument(:event) {} + end + + it "should publish the event even when block raised an exception" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event } + lambda { Instrumentation.instrument(:event) { raise "not working" } }.should raise_error + end + + it "should retain start end finish time of the event" do + Instrumentation.expects(:publish).with { |label,event,data| data.include?(:started) and data.include?(:finished) } + Instrumentation.instrument(:event) {} + end + end + + describe "without a block" do + it "should raise an error if stop is called with no matching start" do + lambda{ Instrumentation.stop(:event) }.should raise_error + end + + it "should publish an event on stop" do + Instrumentation.expects(:publish).with { |label,event,data| event == :start } + Instrumentation.expects(:publish).with { |label,event,data| event == :stop and data.include?(:started) and data.include?(:finished) } + data = {} + Instrumentation.start(:event, data) + Instrumentation.stop(:event, 1, data) + end + + it "should return a different id per event" do + data = {} + Instrumentation.start(:event, data).should == 1 + Instrumentation.start(:event, data).should == 2 + end + end + end +end \ No newline at end of file