# frozen_string_literal: true 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 # This meta class instance variable is used as a guard to ensure that # before_each, and after_each are only called once. This problem occurs # when there are more than one puppet test infrastructure orchestrator in use. # The use of both puppetabs-spec_helper, and rodjek-rspec_puppet will cause # two resets of the puppet environment, and will cause problem rolling back to # a known point as there is no way to differentiate where the calls are coming # from. See more information in #before_each_test, and #after_each_test # Note that the variable is only initialized to 0 if nil. This is important # as more than one orchestrator will call initialize. A second call can not # simply set it to 0 since that would potentially destroy an active guard. # @@reentry_count ||= 0 @environmentpath = Dir.mktmpdir('environments') Dir.mkdir("#{@environmentpath}/production") owner = Process.pid Puppet.push_context(Puppet.base_context({ :environmentpath => @environmentpath, :basemodulepath => "", }), "Initial for specs") Puppet::Parser::Functions.reset ObjectSpace.define_finalizer(Puppet.lookup(:environments), proc { if Process.pid == owner FileUtils.rm_rf(@environmentpath) end }) Puppet::SSL::Oids.register_puppet_oids end # Call this method once, when beginning a test run--prior to running # any individual tests. # @return nil def self.before_all_tests # The process environment is a shared, persistent resource. $old_env = ENV.to_hash 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 # The name of the rollback mark used in the Puppet.context. This is what # the test infrastructure returns to for each test. # ROLLBACK_MARK = "initial testing state" # Call this method once per test, prior to execution of each individual test. # @return nil def self.before_each_test # When using both rspec-puppet and puppet-rspec-helper, there are two packages trying # to be helpful and orchestrate the callback sequence. We let only the first win, the # second callback results in a no-op. # Likewise when entering after_each_test(), a check is made to make tear down happen # only once. # return unless @@reentry_count == 0 @@reentry_count = 1 Puppet.mark_context(ROLLBACK_MARK) # 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).value, :@cache_class => indirector.instance_variable_get(:@cache_class).value, # dup the termini hash so termini created and registered during # the test aren't stored in our saved_indirection_state :@termini => indirector.instance_variable_get(:@termini).dup } end # 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', {}, { "trusted_testhelper" => true }), ssl_context: Puppet::SSL::SSLContext.new(cacerts: []).freeze, http_session: proc { Puppet.runtime[:http].create_session } }, "Context for specs" ) # trigger `require 'facter'` Puppet.runtime[:facter] Puppet::Parser::Functions.reset Puppet::Application.clear! Puppet::Util::Profiler.clear Puppet::Node::Facts.indirection.terminus_class = :memory facts = Puppet::Node::Facts.new(Puppet[:node_name_value]) Puppet::Node::Facts.indirection.save(facts) Puppet.clear_deprecation_warnings end # Call this method once per test, after execution of each individual test. # @return nil def self.after_each_test # Ensure that a matching tear down only happens once per completed setup # (see #before_each_test). return unless @@reentry_count == 1 @@reentry_count = 0 Puppet.settings.send(:clear_everything_for_tests) Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset Puppet.runtime.clear 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| if variable == :@termini indirector.instance_variable_set(variable, value) else indirector.instance_variable_get(variable).value = value end end end $saved_indirection_state = nil # Restore the global process environment. ENV.replace($old_env) # Clear all environments Puppet.lookup(:environments).clear_all # 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.rollback_context(ROLLBACK_MARK) 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", :publicdir => "/dev/null", :codedir => "/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) # 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[: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