#!/usr/bin/env ruby # Copyright 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. require 'rubygems' require 'find' require 'optparse' module NoopTests GLOBALS_SPEC = 'globals/globals_spec.rb' RSPEC_OPTIONS = '--format documentation --color --tty --backtrace' ASTUTE_YAML_VAR = 'SPEC_ASTUTE_FILE_NAME' BUNDLE_DIR = '.bundled_gems' BUNDLE_VAR = 'GEM_HOME' PUPPET_GEM_VERSION = '~> 3.8.0' TEST_LIBRARY_DIR = 'spec/hosts' GLOBALS_YAML_FOLDER = 'globals' def self.options return @options if @options @options = {} optparse = OptionParser.new do|opts| opts.separator 'Main options:' opts.on('-b', '--bundle', 'Use bundle to setup environment') do @options[:bundle] = true end opts.on('-m', '--missing', 'Find missing spec files') do @options[:missing_specs] = true end opts.on('-i', '--individually', 'Run each spec individually') do @options[:run_individually] = true end opts.on('-a', '--astute_yaml_dir DIR', 'Path to astute_yaml folder') do |dir| @options[:astute_yaml_dir] = dir ENV['SPEC_YAML_DIR'] = dir end opts.on('-Y', '--list_yamls', 'List all astute yaml files') do @options[:list_yamls] = true end opts.on('-S', '--list_specs', 'List all noop spec files') do @options[:list_specs] = true end opts.on('-g', '--skip_globals', "Don't run 'globals' task") do @options[:skip_globals] = true end opts.on('-A', '--failed_log FILE', 'Log failed specs and yamls to this file') do |file| @options[:failed_log] = file end opts.on('-E', '--run_failed_log FILE', 'Run only failed specs and yamls from the failed log') do |file| @options[:run_failed_log] = file end opts.separator 'Filter options:' opts.on('-s', '--specs SPEC1,SPEC2', Array, 'Run only these specs. Example: "hosts/hosts_spec.rb"') do |specs| specs = specs.map do |spec| spec.strip! spec.gsub! '.pp', '' if spec.end_with? '.pp' spec += '_spec.rb' unless spec.end_with? '_spec.rb' spec end @options[:filter_specs] = specs end opts.on('-y', '--yamls YAML1,YAML2', Array, 'Run only these yamls. Example: "novanet-primary-controller.yaml"') do |yamls| @options[:filter_yamls] = yamls end opts.on('-e', '--examples STR1,STR2', Array, 'Run only these exemples. Example: "should compile"') do |examples| @options[:filter_examples] = examples end opts.separator 'Debug options:' opts.on('-c', '--console', 'Run PRY console') do @options[:console] = true end opts.on('-d', '--debug', 'Show debug') do @options[:debug] = true end opts.separator 'Spec options:' opts.on('-F', '--file_resources DIR', 'Save file resources to this dir') do |dir| ENV['SPEC_SAVE_FILE_RESOURCES'] = dir end opts.on('-C', '--catalog_show', 'Show catalog debug output') do ENV['SPEC_CATALOG_SHOW'] = 'YES' end opts.on('-Q', '--catalog_save', 'Save catalog to the files instead of comparing them with the current catalogs') do ENV['SPEC_CATALOG_CHECK'] = 'save' end opts.on('-q', '--catalog_check', 'Check the saved catalog against the current one') do ENV['SPEC_CATALOG_CHECK'] = 'check' end opts.on('-G', '--spec_generate', 'Generate specs for catalogs') do ENV['SPEC_SPEC_GENERATE'] = 'YES' end opts.on('-T', '--spec_status', 'Show spec status blocks') do ENV['SPEC_SHOW_STATUS'] = 'YES' end opts.on('-O', '--spec_coverage', 'Show spec coverage statistics') do ENV['SPEC_COVERAGE'] = 'YES' end opts.on('-D', '--test_ubuntu', 'Run tests for Ubuntu facts') do ENV['SPEC_TEST_UBUNTU'] = 'YES' end opts.on('-R', '--test_centos', 'Run tests for CentOS facts') do ENV['SPEC_TEST_CENTOS'] = 'YES' end opts.on('-p', '--puppet_debug', 'Show Puppet debug messages') do ENV['SPEC_PUPPET_DEBUG'] = 'YES' end opts.on('-B', '--puppet_binary_files', 'Check if Puppet installs binary files') do ENV['SPEC_PUPPET_BINARY_FILES'] = 'YES' end opts.on('-L', '--puppet_logs_dir DIR', 'Save Puppet logs in this directory') do |dir| ENV['SPEC_PUPPET_LOGS_DIR'] = dir @options[:puppet_logs_dir] = dir end opts.on('-u', '--update-librarian-puppet', 'Run librarian-puppet update in the deployment directory prior to testing') do @options[:update_librarian_puppet] = true end opts.on('-r', '--reset-librarian-puppet', 'Reset puppet modules to librarian versions in the deployment directory prior to testing') do @options[:reset_librarian_puppet] = true end end optparse.parse! @options end # PATHS # # workspace directory where gem bundle will be created # is passed from Jenkins or default value is used # @@return [String] def self.workspace workspace = ENV['WORKSPACE'] unless workspace workspace = '/tmp/noop' Dir.mkdir workspace unless File.directory? workspace end unless File.directory? workspace raise "Workspace '#{workspace}' is not a directory!" end workspace end # the root directory of noop tests # @return [String] def self.noop_tests_directory return @noop_tests_directory if @noop_tests_directory @noop_tests_directory = File.expand_path File.absolute_path File.join File.dirname(__FILE__), '..', '..', 'tests', 'noop' end # the folder where astute yaml files are found # can be overridden by options # @return [String] def self.astute_yaml_directory return options[:astute_yaml_dir] if options[:astute_yaml_dir] and File.directory? options[:astute_yaml_dir] File.join noop_tests_directory, 'astute.yaml' end # the directory where actual tests library is found # @return [String] def self.test_spec_directory File.join noop_tests_directory, TEST_LIBRARY_DIR end # the directory where the library of modular tasks can be found # @return [String] def self.modular_tasks_directory File.expand_path File.join File.dirname(__FILE__), '..', '..', 'deployment', 'puppet', 'osnailyfacter', 'modular' end # the base directory that houses all the puppet module directory # @return [String] def self.deployment_directory File.expand_path File.join File.dirname(__FILE__), '..', '..', 'deployment' end # LISTERS # # find all modular task files # @return [Array] def self.modular_puppet_task_files files = [] Find.find(modular_tasks_directory) do |file| next unless File.file? file next unless file.end_with? '.pp' file.gsub! modular_tasks_directory + '/', '' files << file end files end # find all astute yaml files # @return [Array] def self.astute_yaml_files files = [] Dir.entries(astute_yaml_directory).each do |file| next unless File.file? File.join astute_yaml_directory, file next unless file.end_with? '.yaml' files << file end files end # find all noop spec files # @return [Array] def self.noop_spec_files files = [] Find.find(test_spec_directory) do |file| next unless File.file? file next unless file.end_with? '_spec.rb' file.gsub! test_spec_directory + '/', '' files << file end files end # ACTIONS # # run the code block inside the tests directory # and then return back def self.inside_noop_tests_directory current_directory = Dir.pwd Dir.chdir noop_tests_directory result = yield Dir.chdir current_directory if current_directory result end # run the clode block inside the deployment driectory # and then return back def self.inside_deployment_directory current_directory = Dir.pwd Dir.chdir deployment_directory result = yield Dir.chdir current_directory if current_directory result end # find all modular tasks that have no corresponding specs # @return [Array] def self.puppet_tasks_without_specs tasks = [] modular_puppet_task_files.each do |task| spec = task.gsub '.pp', '_spec.rb' spec_path = File.join test_spec_directory, spec tasks << task unless File.exist? spec_path end tasks end # append relative path prefix to a spec name # @param [String] spec Spec name # @return [String] def self.spec_path(spec) "'#{File.join TEST_LIBRARY_DIR, spec}'" end # run the rake spec_prep to get fixtures # return: success # @return success def self.rake_prep inside_noop_tests_directory do command_prep = "rake spec_prep" command_prep = 'bundle exec ' + command_prep if options[:bundle] debug " RUN: #{command_prep}" system command_prep $?.exitstatus == 0 end end # run the rspec commands with some options # return: [ success, report ] # @param [String] spec Spec file or pattern # @return [Array] success and empty report array def self.rspec(spec) inside_noop_tests_directory do command = "rspec #{RSPEC_OPTIONS} #{spec}" command = 'bundle exec ' + command if options[:bundle] if options[:filter_examples] options[:filter_examples].each do |example| command = command + " -e #{example}" end end if options[:puppet_logs_dir] command = command + " --deprecation-out #{File.join options[:puppet_logs_dir], 'deprecations.log'}" end debug "RUN: #{command}" system command [ $?.exitstatus == 0, nil ] end end # run all specs together using pattern # return: [ success, report ] # @return [Array] success and empty report array def self.run_all_specs include_prefix = '--pattern' exclude_prefix = '--exclude-pattern' exclude_pattern = "#{exclude_prefix} #{spec_path GLOBALS_SPEC}" if options[:filter_specs] include_pattern = "#{include_prefix} #{options[:filter_specs].map { |s| spec_path s }.join ','}" else include_pattern = "#{include_prefix} #{spec_path '**/*_spec.rb'}" end rspec "#{exclude_pattern} #{include_pattern}" end # run the globals task for the given yaml file # @param [String] astute_yaml YAML file def self.globals(astute_yaml) return if options[:skip_globals] globals_file = File.join astute_yaml_directory, GLOBALS_YAML_FOLDER, astute_yaml if File.file? globals_file begin File.unlink globals_file debug "Globals file was removed: '#{globals_file}'" rescue => e debug "Could not remove globals file: '#{globals_file}'! (#{e.message})" end end rspec spec_path(GLOBALS_SPEC) end # output a debug line is debug is enabled # @param [String] msg The message line def self.debug(msg) puts msg if options[:debug] end # decide if this spec should be included # @param [String] spec Spec file # @return [TrueClass,FalseClass] def self.filter_specs(spec) return false if spec == GLOBALS_SPEC return true unless options[:filter_specs] options[:filter_specs].include? spec end # decide if this yaml should be included # @param [String] yaml Yaml file # @return [TrueClass,FalseClass] def self.filter_yamls(yaml) return true unless options[:filter_yamls] options[:filter_yamls].map { |y| y.gsub('.yaml', '') }.include? yaml.gsub('.yaml', '') end # run the code block for every astute yaml file # return [ global success, Hash of reports for every yaml ] # @return [Array] def self.for_every_astute_yaml prepare_bundle if options[:bundle] results = {} errors = 0 astute_yaml_files.each do |astute_yaml| next unless filter_yamls astute_yaml ENV[ASTUTE_YAML_VAR] = astute_yaml debug "=== YAML: '#{astute_yaml}' ===" globals astute_yaml success, report = yield errors += 1 unless success results[astute_yaml] = { :success => success, :report => report, } end [ errors == 0, results ] end # run every spec file individually # return report for every spec # return [ global success, Hash of reports for every spec ] # @return [Array] def self.run_all_specs_individually results = {} errors = 0 noop_spec_files.each do |spec| next if spec == GLOBALS_SPEC next unless filter_specs spec debug "--- SPEC: '#{spec}' ---" success, report = rspec spec_path(spec) errors += 1 unless success results[spec] = { :success => success, :report => report, } end [ errors == 0, results ] end # directly run the specific spec with a specific yaml # @param yaml [String] yaml file name # @param spec [String] spec name # @return [Array] success and results array def self.run_yaml_and_spec_pair(yaml, spec=nil) ENV[ASTUTE_YAML_VAR] = yaml globals yaml rspec spec_path(spec) end # setup the bundle directory def self.prepare_bundle ENV['PUPPET_GEM_VERSION'] = PUPPET_GEM_VERSION unless ENV['PUPPET_GEM_VERSION'] inside_noop_tests_directory do `bundle --version` raise 'Bundle is not installed!' if $?.exitstatus != 0 ENV[BUNDLE_VAR] = File.join workspace, BUNDLE_DIR system 'bundle install' system 'bundle update' raise 'Could not prepare bundle environment!' if $?.exitstatus != 0 end end # run librarian-puppet to fetch modules as necessary def self.prepare_library # these are needed to ensure we have the correctly bundle ENV['PUPPET_GEM_VERSION'] = PUPPET_GEM_VERSION unless ENV['PUPPET_GEM_VERSION'] ENV['BUNDLE_DIR'] = File.join workspace, BUNDLE_DIR command = './update_modules.sh -v' # pass the bundle parameter to update_modules if specified for this script command = command + ' -b' if options[:bundle] # pass the reset parameter to update_modules if specified for this script command = command + ' -r' if options[:reset_librarian_puppet] inside_deployment_directory do puts "-> Starting update_modules script" system command raise 'Unable to update upstream puppet modules using librarian-puppet' if $?.exitstatus != 0 puts "-> Finished update_modules script" end end # add color codes to a line # @param [Integer] code Color code # @param [String] string Text string def self.color(code, string) "\033[#{code}m#{string}\033[0m" end def self.status_string(success) if success color 32, 'OK' else color 31, 'FAIL' end end # calculate the maximum length of the hash keys # used to align columns # @param [Hash] # @return [Integer] def self.max_key_length(hash) hash.keys.inject(0) do |ml, key| key = key.to_s ml = key.size if key.size > ml ml end end def self.save_failed_log(results) return unless options[:failed_log] File.open(options[:failed_log], 'w') do |file| results.each do |astute_yaml, yaml_result| if yaml_result[:report].is_a? Hash yaml_result.fetch(:report, {}).each do |spec, spec_result| file.puts "#{astute_yaml} #{spec}" unless spec_result[:success] end else file.puts astute_yaml unless yaml_result[:success] end end end end # run specs and yamls found in the failed log file # it can be either just a list of yaml names # or yaml name and spec name speparated by space # log file can be generated by -A options or written manually # @param log_file [String] path to the failed log file # @return [Hash] results structure def self.run_failed_log(log_file) raise "Unable to read failed log file: '#{log_file}'!" unless File.readable? log_file log = File.read log_file results = {} errors = 0 log.split("\n").each do |line| line_array = line.strip.split yaml = line_array[0] spec = line_array[1] next unless yaml debug "=== YAML: '#{yaml}' ===" debug "--- SPEC: '#{spec}' ---" if spec unless results[yaml] results[yaml] = { :success => true, :report => {}, } end success, report = run_yaml_and_spec_pair yaml, spec errors += 1 unless success spec_result = { :success => success, :report => report, } if spec results[yaml][:sucess] = false unless spec_result[:success] results[yaml][:report][spec] = {} unless results[yaml][:report] results[yaml][:report][spec] = spec_result else results[yaml] = spec_result end end [ errors == 0, results ] end # output the test results # @param [Hash] def self.show_results(results) max_astute_yaml_length = max_key_length results results.each do |astute_yaml, yaml_result| puts "-> #{astute_yaml.ljust max_astute_yaml_length} #{status_string yaml_result[:success]}" if yaml_result[:report].is_a? Hash max_spec_length = max_key_length yaml_result[:report] yaml_result[:report].each do |spec, spec_result| puts " * #{spec.ljust max_spec_length} #{status_string spec_result[:success]}" end end end end # the main function def self.main if options[:console] require 'pry' self.pry exit 0 end # ensure fixtures rake_prep if options[:missing_specs] missing_specs = puppet_tasks_without_specs if missing_specs.any? puts color(31, "Missing specs for tasks: #{missing_specs.join ', '}") exit missing_specs.length end end if options[:list_yamls] astute_yaml_files.each do |file| puts file end exit 0 end if options[:list_specs] noop_spec_files.each do |file| puts file end exit 0 end if options[:update_librarian_puppet] prepare_library end # failed log replay if options[:run_failed_log] success, result = run_failed_log options[:run_failed_log] show_results result save_failed_log result exit 1 unless success exit 0 end debug "Spec filter: #{options[:filter_specs]}" if options[:filter_specs] debug "Yaml filter: #{options[:filter_yamls]}" if options[:filter_yamls] success, result = for_every_astute_yaml do if options[:run_individually] run_all_specs_individually else run_all_specs end end show_results result save_failed_log result exit 1 unless success exit 0 end end NoopTests.main if __FILE__ == $0