diff --git a/lib/puppet.rb b/lib/puppet.rb index 5f056e5de..bf54dd764 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,257 +1,267 @@ require 'puppet/version' # see the bottom of the file for further inclusions # Also see the new Vendor support - towards the end # require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' require 'puppet/util/run_mode' require 'puppet/external/pson/common' require 'puppet/external/pson/version' require 'puppet/external/pson/pure' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' # The main Puppet class. Everything is contained here. # # @api public module Puppet require 'puppet/file_system' require 'puppet/context' require 'puppet/environments' class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Settings.new # The services running in this process. @services ||= [] require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.define_settings(section, hash) @@settings.define_settings(section, hash) end # Get the value for a setting # # @param [Symbol] param the setting to retrieve # # @api public def self.[](param) if param == :debug return Puppet::Util::Log.level == :debug else return @@settings[param] end end # setting access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.settings @@settings end def self.run_mode # This sucks (the existence of this method); there are a lot of places in our code that branch based the value of # "run mode", but there used to be some really confusing code paths that made it almost impossible to determine # when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle # stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value. # # I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in # the code that call this method at the moment... so I've settled for isolating it inside of the Settings class # (rather than using a global variable, as we did previously...). Would be good to revisit this at some point. # # --cprice 2012-03-16 Puppet::Util::RunMode[@@settings.preferred_run_mode] end # Load all of the settings. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. # @deprecated Use {initialize_settings} def self.parse_config() Puppet.deprecation_warning("Puppet.parse_config is deprecated; please use Faces API (which will handle settings and state management for you), or (less desirable) call Puppet.initialize_settings") Puppet.initialize_settings end # Initialize puppet's settings. This is intended only for use by external tools that are not # built off of the Faces API or the Puppet::Util::Application class. It may also be used # to initialize state so that a Face may be used programatically, rather than as a stand-alone # command-line tool. # # @api public # @param args [Array] the command line arguments to use for initialization # @return [void] def self.initialize_settings(args = []) do_initialize_settings_for_run_mode(:user, args) end # Initialize puppet's settings for a specified run_mode. # # @deprecated Use {initialize_settings} def self.initialize_settings_for_run_mode(run_mode) Puppet.deprecation_warning("initialize_settings_for_run_mode may be removed in a future release, as may run_mode itself") do_initialize_settings_for_run_mode(run_mode, []) end # private helper method to provide the implementation details of initializing for a run mode, # but allowing us to control where the deprecation warning is issued def self.do_initialize_settings_for_run_mode(run_mode, args) Puppet.settings.initialize_global_settings(args) run_mode = Puppet::Util::RunMode[run_mode] Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(run_mode)) Puppet.push_context(Puppet.base_context(Puppet.settings), "Initial context after settings initialization") Puppet::Parser::Functions.reset Puppet::Util::Log.level = Puppet[:log_level] end private_class_method :do_initialize_settings_for_run_mode # Create a new type. Just proxy to the Type class. The mirroring query # code was deprecated in 2008, but this is still in heavy use. I suppose # this can count as a soft deprecation for the next dev. --daniel 2011-04-12 def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Load vendored (setup paths, and load what is needed upfront). # See the Vendor class for how to add additional vendored gems/code require "puppet/vendor" Puppet::Vendor.load_vendored # Set default for YAML.load to unsafe so we don't affect programs # requiring puppet -- in puppet we will call safe explicitly SafeYAML::OPTIONS[:default_mode] = :unsafe # The bindings used for initialization of puppet # @api private def self.base_context(settings) environments = settings[:environmentpath] modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath]) if environments.empty? loaders = [Puppet::Environments::Legacy.new] else loaders = Puppet::Environments::Directories.from_path(environments, modulepath) # in case the configured environment (used for the default sometimes) # doesn't exist loaders << Puppet::Environments::StaticPrivate.new( Puppet::Node::Environment.create(Puppet[:environment].to_sym, [], Puppet::Node::Environment::NO_MANIFEST)) end { :environments => Puppet::Environments::Cached.new(*loaders) } end # A simple set of bindings that is just enough to limp along to # initialization where the {base_context} bindings are put in place # @api private def self.bootstrap_context root_environment = Puppet::Node::Environment.create(:'*root*', [], '') { :current_environment => root_environment, :root_environment => root_environment } end # @param overrides [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @api private def self.push_context(overrides, description = "") @context.push(overrides, description) end # Return to the previous context. # @raise [StackUnderflow] if the current context is the root # @api private def self.pop_context @context.pop end # Lookup a binding by name or return a default value provided by a passed block (if given). # @api private def self.lookup(name, &block) @context.lookup(name, &block) end # @param bindings [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @yield [] A block executed in the context of the temporarily pushed bindings. # @api private def self.override(bindings, description = "", &block) @context.override(bindings, description, &block) end + # @api private + def self.mark_context(name) + @context.mark(name) + end + + # @api private + def self.rollback_context(name) + @context.rollback(name) + end + require 'puppet/node' # The single instance used for normal operation @context = Puppet::Context.new(bootstrap_context) end # This feels weird to me; I would really like for us to get to a state where there is never a "require" statement # anywhere besides the very top of a file. That would not be possible at the moment without a great deal of # effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16 require 'puppet/indirector' require 'puppet/type' require 'puppet/resource' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/data_binding' require 'puppet/util/storage' require 'puppet/status' require 'puppet/file_bucket/file' diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index da74518e2..9f533b12d 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -1,485 +1,483 @@ require 'optparse' require 'puppet/util/command_line' require 'puppet/util/plugins' require 'puppet/util/constant_inflector' require 'puppet/error' module Puppet # 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 default +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 # affect 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 class Application require 'puppet/util' include Puppet::Util DOCPATTERN = ::File.expand_path(::File.dirname(__FILE__) + "/util/command_line/*" ) CommandLineArgs = Struct.new(:subcommand_name, :args) @loader = Puppet::Util::Autoload.new(self, 'puppet/application') 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 SHOULD_PARSE_CONFIG_DEPRECATION_MSG = "is no longer supported; config file parsing " + "is now controlled by the puppet engine, rather than by individual applications. This " + "method will be removed in a future version of puppet." def should_parse_config Puppet.deprecation_warning("should_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_not_parse_config Puppet.deprecation_warning("should_not_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_parse_config? Puppet.deprecation_warning("should_parse_config? " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) true end # used to declare code that handle an option def option(*options, &block) long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') fname = "handle_#{long}".intern 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 # @return [Array] the names of available applications # @api public def available_application_names @loader.files_to_load.map do |fn| ::File.basename(fn, '.rb') end.uniq end # Finds the class for a given application and loads the class. This does # not create an instance of the application, it only gets a handle to the # class. The code for the application is expected to live in a ruby file # `puppet/application/#{name}.rb` that is available on the `$LOAD_PATH`. # # @param application_name [String] the name of the application to find (eg. "apply"). # @return [Class] the Class instance of the application that was found. # @raise [Puppet::Error] if the application class was not found. # @raise [LoadError] if there was a problem loading the application file. # @api public def find(application_name) begin require @loader.expand(application_name.to_s.downcase) rescue LoadError => e Puppet.log_and_raise(e, "Unable to find application '#{application_name}'. #{e}") end class_name = Puppet::Util::ConstantInflector.file2constant(application_name.to_s) clazz = try_load_class(class_name) ################################################################ #### Begin 2.7.x backward compatibility hack; #### eventually we need to issue a deprecation warning here, #### and then get rid of this stanza in a subsequent release. ################################################################ if (clazz.nil?) class_name = application_name.capitalize clazz = try_load_class(class_name) end ################################################################ #### End 2.7.x backward compatibility hack ################################################################ if clazz.nil? raise Puppet::Error.new("Unable to load application class '#{class_name}' from file 'puppet/application/#{application_name}.rb'") end return clazz end # Given the fully qualified name of a class, attempt to get the class instance. # @param [String] class_name the fully qualified name of the class to try to load # @return [Class] the Class instance, or nil? if it could not be loaded. def try_load_class(class_name) return self.const_defined?(class_name) ? const_get(class_name) : nil end private :try_load_class 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) if mode_name Puppet.settings.preferred_run_mode = mode_name end return @run_mode if @run_mode and not mode_name require 'puppet/util/run_mode' @run_mode = Puppet::Util::RunMode[ mode_name || Puppet.settings.preferred_run_mode ] end # This is for testing only def clear_everything_for_tests @run_mode = @banner = @run_status = @option_parser_commands = nil end end attr_reader :options, :command_line # Every app responds to --version # See also `lib/puppet/util/command_line.rb` for some special case early # handling of this. 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 app_defaults() Puppet::Settings.app_defaults_for_run_mode(self.class.run_mode).merge( :name => name ) end def initialize_app_defaults() Puppet.settings.initialize_app_defaults(app_defaults) end # override to execute code before running anything else def preinit end def initialize(command_line = Puppet::Util::CommandLine.new) @command_line = CommandLineArgs.new(command_line.subcommand_name, command_line.args.dup) @options = {} end # Execute the application. # @api public # @return [void] def run # I don't really like the names of these lifecycle phases. It would be nice to change them to some more meaningful # names, and make deprecated aliases. Also, Daniel suggests that we can probably get rid of this "plugin_hook" # pattern, but we need to check with PE and the community first. --cprice 2012-03-16 # exit_on_fail("get application-specific default settings") do plugin_hook('initialize_app_defaults') { initialize_app_defaults } end - Puppet.override(Puppet.base_context(Puppet.settings)) do - configured_environment = Puppet.lookup(:environments).get(Puppet[:environment]) - configured_environment = configured_environment.override_from_commandline(Puppet.settings) - - # Setup a new context using the app's configuration - Puppet.override({ :current_environment => configured_environment }, - "New base context and current environment from application's configuration") do - require 'puppet' - require 'puppet/util/instrumentation' - Puppet::Util::Instrumentation.init - - exit_on_fail("initialize") { plugin_hook('preinit') { preinit } } - exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } } - exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } } - exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes } - exit_on_fail("run") { plugin_hook('run_command') { run_command } } - end - end + Puppet.push_context(Puppet.base_context(Puppet.settings), "Update for application settings (#{self.class.run_mode})") + configured_environment = Puppet.lookup(:environments).get(Puppet[:environment]) + configured_environment = configured_environment.override_from_commandline(Puppet.settings) + + # Setup a new context using the app's configuration + Puppet.push_context({ :current_environment => configured_environment }, + "Update current environment from application's configuration") + + require 'puppet/util/instrumentation' + Puppet::Util::Instrumentation.init + + exit_on_fail("initialize") { plugin_hook('preinit') { preinit } } + exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } } + exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } } + exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes } + exit_on_fail("run") { plugin_hook('run_command') { run_command } } end def main raise NotImplementedError, "No valid command or main" end def run_command main end def setup setup_logs end def setup_logs if options[:debug] || options[:verbose] Puppet::Util::Log.newdestination(:console) end set_log_level Puppet::Util::Log.setup_default unless options[:setdest] end def set_log_level if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end end def handle_logdest_arg(arg) begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail Puppet.log_exception(detail) end end def configure_indirector_routes route_file = Puppet[:route_file] if Puppet::FileSystem.exist?(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) # Here we're building up all of the options that the application may need to handle. The main # puppet settings defined in "defaults.rb" have already been parsed once (in command_line.rb) by # the time we get here; however, our app may wish to handle some of them specially, so we need to # make the parser aware of them again. We might be able to make this a bit more efficient by # re-using the parser object that gets built up in command_line.rb. --cprice 2012-03-16 # 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, val) opt, val = Puppet::Settings.clean_opt(opt, val) send(:handle_unknown, opt, val) if respond_to?(:handle_unknown) 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 def plugin_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 private :plugin_hook end end diff --git a/lib/puppet/context.rb b/lib/puppet/context.rb index f01a399b3..d6f064c28 100644 --- a/lib/puppet/context.rb +++ b/lib/puppet/context.rb @@ -1,60 +1,101 @@ # Puppet::Context is a system for tracking services and contextual information # that puppet needs to be able to run. Values are "bound" in a context when it is created # and cannot be changed; however a child context can be created, using # {#override}, that provides a different value. # # When binding a {Proc}, the proc is called when the value is looked up, and the result # is memoized for subsequent lookups. This provides a lazy mechanism that can be used to # delay expensive production of values until they are needed. # # @api private class Puppet::Context require 'puppet/context/trusted_information' class UndefinedBindingError < Puppet::Error; end class StackUnderflow < Puppet::Error; end + class UnknownRollbackMarkError < Puppet::Error; end + class DuplicateRollbackMarkError < Puppet::Error; end + # @api private def initialize(initial_bindings) - @stack = [] @table = initial_bindings @description = "root" + @id = 0 + @rollbacks = {} + @stack = [[0, nil, nil]] end # @api private def push(overrides, description = "") - @stack.push([@table, @description]) + @id += 1 + @stack.push([@id, @table, @description]) @table = @table.merge(overrides || {}) @description = description end # @api private def pop - if @stack.empty? + if @stack[-1][0] == 0 raise(StackUnderflow, "Attempted to pop, but already at root of the context stack.") else - (@table, @description) = @stack.pop + (_, @table, @description) = @stack.pop end end # @api private def lookup(name, &block) if @table.include?(name) value = @table[name] value.is_a?(Proc) ? (@table[name] = value.call) : value elsif block block.call else raise UndefinedBindingError, "no '#{name}' in #{@table.inspect} at top of #{@stack.inspect}" end end # @api private def override(bindings, description = "", &block) + mark_point = "override over #{@stack[-1][0]}" + mark(mark_point) push(bindings, description) yield ensure - pop + rollback(mark_point) + end + + # Mark a place on the context stack to later return to with {rollback}. + # + # @param name [Object] The identifier for the mark + # + # @api private + def mark(name) + if @rollbacks[name].nil? + @rollbacks[name] = @stack[-1][0] + else + raise DuplicateRollbackMarkError, "Mark for '#{name}' already exists" + end + end + + # Roll back to a mark set by {mark}. + # + # Rollbacks can only reach a mark accessible via {pop}. If the mark is not on + # the current context stack the behavior of rollback is undefined. + # + # @param name [Object] The identifier for the mark + # + # @api private + def rollback(name) + if @rollbacks[name].nil? + raise UnknownRollbackMarkError, "Unknown mark '#{name}'" + end + + while @stack[-1][0] != @rollbacks[name] + pop + end + + @rollbacks.delete(name) end end diff --git a/lib/puppet/test/test_helper.rb b/lib/puppet/test/test_helper.rb index 3c6bd4dd3..61b66c6eb 100644 --- a/lib/puppet/test/test_helper.rb +++ b/lib/puppet/test/test_helper.rb @@ -1,210 +1,211 @@ require 'puppet/indirector/data_binding/hiera' require 'tmpdir' require 'fileutils' module Puppet::Test # This class is intended to provide an API to be used by external projects # when they are running tests that depend on puppet core. This should # allow us to vary the implementation details of managing puppet's state # for testing, from one version of puppet to the next--without forcing # the external projects to do any of that state management or be aware of # the implementation details. # # For now, this consists of a few very simple signatures. The plan is # that it should be the responsibility of the puppetlabs_spec_helper # to broker between external projects and this API; thus, if any # hacks are required (e.g. to determine whether or not a particular) # version of puppet supports this API, those hacks will be consolidated in # one place and won't need to be duplicated in every external project. # # This should also alleviate the anti-pattern that we've been following, # wherein each external project starts off with a copy of puppet core's # test_helper.rb and is exposed to risk of that code getting out of # sync with core. # # Since this class will be "library code" that ships with puppet, it does # not use API from any existing test framework such as rspec. This should # theoretically allow it to be used with other unit test frameworks in the # future, if desired. # # Note that in the future this API could potentially be expanded to handle # other features such as "around_test", but we didn't see a compelling # reason to deal with that right now. class TestHelper # Call this method once, as early as possible, such as before loading tests # that call Puppet. # @return nil def self.initialize() owner = Process.pid Puppet.push_context(Puppet.base_context({ :environmentpath => "", :basemodulepath => "", :manifest => "/dev/null" }), "Initial for specs") Puppet::Parser::Functions.reset end # Call this method once, when beginning a test run--prior to running # any individual tests. # @return nil def self.before_all_tests() # Make sure that all of the setup is also done for any before(:all) blocks end # Call this method once, at the end of a test run, when no more tests # will be run. # @return nil def self.after_all_tests() end # Call this method once per test, prior to execution of each invididual test. # @return nil def self.before_each_test() + Puppet.mark_context("initial testing state") # We need to preserve the current state of all our indirection cache and # terminus classes. This is pretty important, because changes to these # are global and lead to order dependencies in our testing. # # We go direct to the implementation because there is no safe, sane public # API to manage restoration of these to their default values. This # should, once the value is proved, be moved to a standard API on the # indirector. # # To make things worse, a number of the tests stub parts of the # indirector. These stubs have very specific expectations that what # little of the public API we could use is, well, likely to explode # randomly in some tests. So, direct access. --daniel 2011-08-30 $saved_indirection_state = {} indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state[indirector.name] = { :@terminus_class => indirector.instance_variable_get(:@terminus_class), :@cache_class => indirector.instance_variable_get(:@cache_class) } end # The process environment is a shared, persistent resource. $old_env = ENV.to_hash # So is the load_path $old_load_path = $LOAD_PATH.dup initialize_settings_before_each() Puppet.push_context( { :trusted_information => Puppet::Context::TrustedInformation.new('local', 'testing', {}), }, "Context for specs") Puppet::Parser::Functions.reset Puppet::Node::Environment.clear Puppet::Application.clear! Puppet::Util::Profiler.clear Puppet.clear_deprecation_warnings Puppet::DataBinding::Hiera.instance_variable_set("@hiera", nil) end # Call this method once per test, after execution of each individual test. # @return nil def self.after_each_test() Puppet.settings.send(:clear_everything_for_tests) Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset Puppet.clear_deprecation_warnings # uncommenting and manipulating this can be useful when tracking down calls to deprecated code #Puppet.log_deprecations_to_file("deprecations.txt", /^Puppet::Util.exec/) # Restore the indirector configuration. See before hook. indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state.fetch(indirector.name, {}).each do |variable, value| indirector.instance_variable_set(variable, value) end end $saved_indirection_state = nil # Restore the global process environment. Can't just assign because this # is a magic variable, sadly, and doesn't do thatâ„¢. It is sufficiently # faster to use the compare-then-set model to avoid excessive work that it # justifies the complexity. --daniel 2012-03-15 unless ENV.to_hash == $old_env ENV.clear $old_env.each {|k, v| ENV[k] = v } end # Some tests can cause us to connect, in which case the lingering # connection is a resource that can cause unexpected failure in later # tests, as well as sharing state accidentally. # We're testing if ActiveRecord::Base is defined because some test cases # may stub Puppet.features.rails? which is how we should normally # introspect for this functionality. ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base) # Restore the load_path late, to avoid messing with stubs from the test. $LOAD_PATH.clear $old_load_path.each {|x| $LOAD_PATH << x } - Puppet.pop_context + Puppet.rollback_context("initial testing state") end ######################################################################################### # PRIVATE METHODS (not part of the public TestHelper API--do not call these from outside # of this class!) ######################################################################################### def self.app_defaults_for_tests() { :logdir => "/dev/null", :confdir => "/dev/null", :vardir => "/dev/null", :rundir => "/dev/null", :hiera_config => "/dev/null", } end private_class_method :app_defaults_for_tests def self.initialize_settings_before_each() Puppet.settings.preferred_run_mode = "user" # Initialize "app defaults" settings to a good set of test values Puppet.settings.initialize_app_defaults(app_defaults_for_tests) # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" # We don't want to depend upon the reported domain name of the # machine running the tests, nor upon the DNS setup of that # domain. Puppet.settings[:use_srv_records] = false # Longer keys are secure, but they sure make for some slow testing - both # in terms of generating keys, and in terms of anything the next step down # the line doing validation or whatever. Most tests don't care how long # or secure it is, just that it exists, so these are better and faster # defaults, in testing only. # # I would make these even shorter, but OpenSSL doesn't support anything # below 512 bits. Sad, really, because a 0 bit key would be just fine. Puppet[:req_bits] = 512 Puppet[:keylength] = 512 # Although we setup a testing context during initialization, some tests # will end up creating their own context using the real context objects # and use the setting for the environments. In order to avoid those tests # having to deal with a missing environmentpath we can just set it right # here. Puppet[:environmentpath] = @environmentpath Puppet[:environment_timeout] = 0 end private_class_method :initialize_settings_before_each end end diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index cc707b1b0..f23774ff7 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -1,97 +1,136 @@ require 'spec_helper' describe Puppet::Context do let(:context) { Puppet::Context.new({ :testing => "value" }) } context "with the implicit test_helper.rb pushed context" do it "fails to lookup a value that does not exist" do expect { context.lookup("a") }.to raise_error(Puppet::Context::UndefinedBindingError) end it "calls a provided block for a default value when none is found" do expect(context.lookup("a") { "default" }).to eq("default") end it "behaves as if pushed a {} if you push nil" do context.push(nil) expect(context.lookup(:testing)).to eq("value") context.pop end it "fails if you try to pop off the top of the stack" do expect { context.pop }.to raise_error(Puppet::Context::StackUnderflow) end end describe "with additional context" do before :each do context.push("a" => 1) end it "holds values for later lookup" do expect(context.lookup("a")).to eq(1) end it "allows rebinding values in a nested context" do inner = nil context.override("a" => 2) do inner = context.lookup("a") end expect(inner).to eq(2) end it "outer bindings are available in an overridden context" do inner_a = nil inner_b = nil context.override("b" => 2) do inner_a = context.lookup("a") inner_b = context.lookup("b") end expect(inner_a).to eq(1) expect(inner_b).to eq(2) end it "overridden bindings do not exist outside of the override" do context.override("a" => 2) do end expect(context.lookup("a")).to eq(1) end it "overridden bindings do not exist outside of the override even when leaving via an error" do begin context.override("a" => 2) do raise "this should still cause the bindings to leave" end rescue end expect(context.lookup("a")).to eq(1) end end + context "a rollback" do + it "returns to the mark" do + context.push("a" => 1) + context.mark("start") + context.push("a" => 2) + context.push("a" => 3) + context.pop + + context.rollback("start") + + expect(context.lookup("a")).to eq(1) + end + + it "rolls back to the mark across a scoped override" do + context.push("a" => 1) + context.mark("start") + context.override("a" => 3) do + + context.rollback("start") + + expect(context.lookup("a")).to eq(1) + end + expect(context.lookup("a")).to eq(1) + end + + it "fails to rollback to an unknown mark" do + expect do + context.rollback("unknown") + end.to raise_error(Puppet::Context::UnknownRollbackMarkError) + end + + it "does not allow the same mark to be set twice" do + context.mark("duplicate") + expect do + context.mark("duplicate") + end.to raise_error(Puppet::Context::DuplicateRollbackMarkError) + end + end + context 'support lazy entries' do it 'by evaluating a bound proc' do result = nil context.override(:a => lambda {|| 'yay'}) do result = context.lookup(:a) end expect(result).to eq('yay') end it 'by memoizing the bound value' do result1 = nil result2 = nil original = 'yay' context.override(:a => lambda {|| tmp = original; original = 'no'; tmp}) do result1 = context.lookup(:a) result2 = context.lookup(:a) end expect(result1).to eq('yay') expect(original).to eq('no') expect(result2).to eq('yay') end end end