class Simp::Rake::Beaker

Public Class Methods

new(base_dir) { |self| ... } click to toggle source
# File lib/simp/rake/beaker.rb, line 12
    def initialize(base_dir)

      @base_dir   = base_dir
      @clean_list = []

      ::CLEAN.include( %{#{@base_dir}/log} )
      ::CLEAN.include( %{#{@base_dir}/junit} )
      ::CLEAN.include( %{#{@base_dir}/sec_results} )
      ::CLEAN.include( %{#{@base_dir}/spec/fixtures/inspec_deps} )

      yield self if block_given?

      ::CLEAN.include( @clean_list )

      namespace :beaker do
        desc <<-EOM
          Run a Beaker test against a specific Nodeset
            * :nodeset - The nodeset against which you wish to run
        EOM
        task :run, [:nodeset] do |t,args|
          fail "You must pass :nodeset to #{t}" unless args[:nodeset]
          nodeset = args[:nodeset].strip

          old_stdout = $stdout
          nodesets = StringIO.new
          $stdout = nodesets

          Rake::Task['beaker_nodes'].invoke

          $stdout = old_stdout

          nodesets = nodesets.string.split("\n")

          fail "Nodeset '#{nodeset}' not found. Valid Nodesets:\n#{nodesets.map{|x| x = %(  * #{x})}.join(%(\n))}" unless nodesets.include?(nodeset)

          ENV['BEAKER_set'] = nodeset

          Rake::Task['beaker'].invoke
        end

        desc <<-EOM
          Run Beaker test suites.
            * :suite - A specific suite to run
              * If you set this to `ALL`, all suites will be run

            * :nodeset - A specific nodeset to run on within a specific suite
              * If you set this to `ALL`, all nodesets will be looped through, starting with 'default'
              * You may also set this to a colon delimited list of short
                nodeset names that will be run in that order

            ## Suite Execution

              By default the only suite that will be executed is `default`.
              Since each suite is executed in a new environment, spin up can
              take a lot of time. Therefore, the default is to only run the
              default suite.

              If there is a suite where the metadata contains `default_run` set
              to the Boolean `true`, then that suite will be part of the
              default suite execution.

              You can run all suites by setting the passed suite name to `ALL`
              (case sensitive).

            ## Environment Variables

              * BEAKER_suite_runall
                * Run all Suites

              * BEAKER_suite_basedir
                * The base directory where suites will be defined
                * Default: spec/acceptance

            ## Global Suite Configuration
              A file `config.yml` can be placed in the `suites` directory to
              control certain aspects of the suite run.

            ### Supported Config:

              ```yaml
              ---
              # Fail the entire suite at the first failure
              'fail_fast' : <true|false> => Default: true
              ```
            ## Individual Suite Configuration

              Each suite may contain a YAML file, metadata.yml, which will be
              used to provide information to the suite of tests.

            ### Supported Config:

              ```yaml
              ---
              'name' :        '<User friendly name for the suite>'

              # Run this suite by default
              'default_run' : <true|false> => Default: false
              ```
        EOM
        task :suites, [:suite, :nodeset] => ['spec_prep'] do |t,args|
          suite = args[:suite]
          nodeset = args[:nodeset]

          # Record Tasks That Fail
          # Need to figure out how to capture the errors
          failures = Hash.new

          suite_basedir = File.join(@base_dir, 'spec/acceptance/suites')

          if ENV['BEAKER_suite_basedir']
            suite_basedir = ENV['BEAKER_suite_basedir']
          end

          raise("Error: Suites Directory at '#{suite_basedir}'!") unless File.directory?(suite_basedir)

          if suite && (suite != 'ALL')
            unless File.directory?(File.join(suite_basedir, suite))
              STDERR.puts("Error: Could not find suite '#{suite}'")
              STDERR.puts("Available Suites:")
              STDERR.puts('  * ' + Dir.glob(
                File.join(suite_basedir, '*')).sort.map{ |x|
                  File.basename(x)
                }.join("\n  * ")
              )
              exit(1)
            end
          end

          suite_config = {
            'fail_fast' => true
          }
          suite_config_metadata_path = File.join(suite_basedir, 'config.yml')
          if File.file?(suite_config_metadata_path)
            suite_config.merge!(YAML.load_file(suite_config_metadata_path))
          end

          suites = Hash.new

          if suite && (suite != 'ALL')
            suites[suite] = Hash.new
            # If a suite was set, make sure it runs.
            suites[suite]['default_run'] = true
          else
            Dir.glob(File.join(suite_basedir,'*')) do |file|
              if File.directory?(file)
                suites[File.basename(file)] = Hash.new

                if suite == 'ALL'
                  suites[File.basename(file)]['default_run'] = true
                end
              end
            end
          end

          suites.keys.each do |ste|
            suites[ste]['name']  = ste
            suites[ste]['path']  = File.join(suite_basedir, ste)

            metadata_path = File.join(suites[ste]['path'], 'metadata.yml')
            if File.file?(metadata_path)
              suites[ste]['metadata'] = YAML.load_file(metadata_path)
            end

            unless File.directory?(File.join(suites[ste]['path'],'nodesets'))
              Dir.chdir(suites[ste]['path']) do
                if File.directory?('../../nodesets')
                  FileUtils.ln_s('../../nodesets', 'nodesets')
                end
              end
            end

            suites[ste].merge!(suites[ste]['metadata']) if suites[ste]['metadata']

            # Ensure that the 'default' suite runs unless explicitly disabled.
            if suites['default']
              if ( suites['default']['default_run'].nil? ) || ( suites['default']['default_run'] == true )
                suites['default']['default_run'] = true
              end
            end
          end

          raise("Error: No Suites Found in '#{suite_basedir}'!") if suites.empty?

          # Need to ensure that 'default' is first
          ordered_suites = suites.keys.sort
          default_suite = ordered_suites.delete('default')
          ordered_suites.unshift(default_suite) if default_suite

          suite_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          ordered_suites.each do |ste|

            next unless (suites[ste]['default_run'] == true)

            name = suites[ste]['name']

            $stdout.puts("\n\n=== Suite '#{name}' Starting ===\n\n")

            nodesets = Array.new
            nodeset_path = File.join(suites[ste]['path'],'nodesets')

            if nodeset
              if nodeset == 'ALL'
                nodesets = Dir.glob(File.join(nodeset_path, '*.yml'))

                # Make sure we run the default set first
                default_set = nodesets.delete(File.join(nodeset_path, 'default.yml'))
                nodesets.unshift(default_set) if default_set
              else
                nodeset.split(':').each do |tgt_nodeset|
                  nodesets << File.join(nodeset_path, "#{tgt_nodeset.strip}.yml")
                end
              end
            else
              nodesets << File.join(nodeset_path, 'default.yml')
            end

            refined_nodesets = []

            nodesets.each do |nodeset_yml|
              parsed_nodeset = ::Beaker::Options::HostsFileParser.parse_hosts_file(nodeset_yml)

              # Default to multi-node testing for backwards compatibility
              multi_node = (parsed_nodeset[:multi_node] == false ? false: true)

              if multi_node
                refined_nodesets.push(nodeset_yml)
              else
                parsed_nodeset_hosts = parsed_nodeset.delete(:HOSTS)

                parsed_nodeset_hosts.each do |k,v|

                  v[:roles] ||= []
                  v[:roles] |= ['default']

                  tmp_nodeset = {
                    :HOSTS  => { k => v },
                    :CONFIG => parsed_nodeset
                  }

                  tmp_nodeset_file = Tempfile.new("nodeset_#{k}-")
                  tmp_nodeset_file.write(tmp_nodeset.to_yaml)
                  tmp_nodeset_file.close

                  refined_nodesets.push(tmp_nodeset_file.path)

                  at_exit do
                    if tmp_nodeset_file && File.exist?(tmp_nodeset_file.path)
                      tmp_nodeset_file.close
                      tmp_nodeset_file.unlink
                    end
                  end
                end
              end
            end

            refined_nodesets.sort.each do |nodeset_yml|
              unless File.file?(nodeset_yml)
                # Get here if user has specified a non-existent nodeset or the
                # implied `default` nodeset does not exist.
                if suite_config['fail_fast']
                  fail("*** Suite #{name} Nodeset '#{File.basename(nodeset_yml, '.yml')}' Not Found ***")
                else
                  $stdout.puts("=== Suite #{name} Nodeset '#{File.basename(nodeset_yml, '.yml')}' Not Found, Skipping ===")
                  next
                end
              end

              ENV['BEAKER_setfile'] = nodeset_yml

              Rake::Task[:beaker].clear
              RSpec::Core::RakeTask.new(:beaker) do |tsk|
                tsk.rspec_opts = ['--color']
                tsk.pattern = File.join(suites[ste]['path'])
              end

              current_suite_task = Rake::Task[:beaker]

              if suite_config['fail_fast'] == true
                current_suite_task.execute
              else
                begin
                  current_suite_task.execute
                rescue SystemExit
                  failures[suites[ste]['name']] = {
                    'path'    => suites[ste]['path'],
                    'nodeset' => File.basename(nodeset_yml, '.yml')
                  }
                end
              end

              $stdout.puts("\n\n=== Suite '#{name}' Complete ===\n\n")
            end
          end
          suite_end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)

          suite_run_time = ((suite_end_time - suite_start_time)/60).round(2)

          $stdout.puts("== Total Runtime: #{suite_run_time} minutes ==\n\n")

          unless failures.keys.empty?
            $stdout.puts("The following tests had failures:")
            failures.keys.sort.each do |ste|
              $stdout.puts("  * #{ste} => #{failures[ste]['path']} on #{failures[ste]['nodeset']}")
            end
          end
        end
      end
    end