#! /usr/bin/ruby # frozen_string_literal: true require 'yaml' require 'ostruct' require 'optparse' require 'fileutils' require 'ectoplasm' require_relative '../lib/spectre' DEFAULT_CONFIG = { 'config_file' => './spectre.yml', 'environment' => 'default', 'specs' => [], 'tags' => [], 'colored' => true, 'verbose' => false, 'reporters' => [ 'Spectre::Reporter::Console', ], 'loggers' => [ 'Spectre::Logger::Console', 'Spectre::Logger::File', ], 'log_file' => './logs/spectre_.log', 'log_format' => { 'console' => { 'indent' => 2, 'width' => 80, 'end_context' => nil, 'separator' => '', }, 'file' => { 'separator' => '-- ', 'start_group' => "-- Start ''", 'end_group' => "-- End ''", }, }, 'debug' => false, 'out_path' => './reports', 'secure_keys' => ['password', 'secret', 'token', 'secure', 'authorization'], 'spec_patterns' => ['./specs/**/*.spec.rb'], 'mixin_patterns' => ['../common/mixins/**/*.mixin.rb', './mixins/**/*.mixin.rb'], 'env_patterns' => ['./environments/**/*.env.yml'], 'env_partial_patterns' => ['./environments/**/*.env.secret.yml'], 'resource_paths' => ['../common/resources', './resources'], 'modules' => [ 'spectre/helpers', 'spectre/reporter/console', 'spectre/reporter/junit', 'spectre/logger/console', 'spectre/logger/file', 'spectre/assertion', 'spectre/diagnostic', 'spectre/environment', 'spectre/mixin', 'spectre/bag', 'spectre/http', 'spectre/http/basic_auth', 'spectre/http/keystone', 'spectre/resources', ], 'include' => [ ], 'exclude' => [ ], } cmd_options = {} property_overrides = {} opt_parser = OptionParser.new do |opts| opts.banner = %{Spectre #{Spectre::VERSION} Usage: spectre [command] [options] Commands: list List specs run Run specs (default) show Print current environment settings dump Dumps the given environment in YAML format to console cleanup Will remove all generated files (e.g. logs and reports) init Initializes a new spectre project Specific options:} opts.on('-s SPEC,SPEC', '--specs SPEC,SPEC', Array, 'The specs to run') do |specs| cmd_options['specs'] = specs end opts.on('-t TAG,TAG', '--tags TAG,TAG', Array, 'Run only specs with given tags') do |tags| cmd_options['tags'] = tags end opts.on('-e NAME', '--env NAME', 'Name of the environment to load') do |env_name| cmd_options['environment'] = env_name end opts.on('-c FILE', '--config FILE', 'Config file to load') do |file_path| cmd_options['config_file'] = file_path end opts.on('--spec-pattern PATTERN', Array, 'File pattern for spec files') do |spec_pattern| cmd_options['spec_patterns'] = spec_pattern end opts.on('--env-pattern PATTERN', Array, 'File pattern for environment files') do |env_patterns| cmd_options['env_patterns'] = env_patterns end opts.on('--no-color', 'Disable colored output') do cmd_options['colored'] = false end opts.on('--ignore-failure', 'Always exit with code 0') do cmd_options['ignore_failure'] = true end opts.on('-o PATH', '--out PATH', 'Output directory path') do |path| cmd_options['out_path'] = File.absolute_path(path) end opts.on('-r NAME', '--reporters NAME', Array, "A list of reporters to use") do |reporters| cmd_options['reporters'] = reporters end opts.on('-d', '--debug', "Run in debug mode") do cmd_options['debug'] = true end opts.on('-p KEY=VAL', '--property KEY=VAL', "Override config option. Use `spectre show` to get list of available options") do |option| key, val = option.split('=') val = val.split(',') if DEFAULT_CONFIG[key].is_a? Array val = ['true', '1'].include? val if [true, false].include?(DEFAULT_CONFIG[key]) val = val.to_i if DEFAULT_CONFIG[key].is_a? Integer opt_path = key.split('.') curr_opt = property_overrides opt_path.each_with_index do |part, i| if i == opt_path.count-1 curr_opt[part] = val break end curr_opt[part] = {} unless curr_opt.key?(part) curr_opt = curr_opt[part] end end opts.separator "\nCommon options:" opts.on_tail('--version', 'Print current installed version') do puts Spectre::VERSION exit end opts.on_tail('-h', '--help', 'Print this help') do puts opts exit end end.parse! action = ARGV[0] || 'run' ########################################### # Load Config ########################################### cfg = {} cfg.deep_merge! DEFAULT_CONFIG global_config_file = File.join File.expand_path('~'), '.spectre' if File.exists? global_config_file global_options = YAML.load_file(global_config_file) cfg.deep_merge! global_options if global_options end config_file = cmd_options['config_file'] || cfg['config_file'] if File.exists? config_file file_options = YAML.load_file(config_file) cfg.deep_merge! file_options Dir.chdir File.dirname(config_file) end cfg.deep_merge! cmd_options ########################################### # Load Environment ########################################### envs = {} read_env_files = {} cfg['env_patterns'].each do |pattern| Dir.glob(pattern).each do|f| spec_env = YAML.load_file(f) || {} name = spec_env['name'] || 'default' if envs.key? name existing_env_file = read_env_files[name] puts "Duplicate environment definition detected with name #{name} in '#{f}'. Previously defined in '#{existing_env_file}'" exit 1 end read_env_files[name] = f envs[name] = spec_env end end # Merge partial environment configs with existing environments cfg['env_partial_patterns'].each do |pattern| Dir.glob(pattern).each do|f| partial_env = YAML.load_file(f) name = partial_env.delete('name') || 'default' next unless envs.key? name envs[name].deep_merge! partial_env end end env = envs[cfg['environment']] cfg.deep_merge! env if env # Merge property overrides after environment load to give it higher priority cfg.deep_merge! property_overrides String.colored! if cfg['colored'] # Load environment exlicitly before loading specs to make it available in spec definition require_relative '../lib/spectre/environment' unless cfg['exclude'].include? 'spectre/environment' Spectre.configure(cfg) ########################################### # Load Specs ########################################### cfg['spec_patterns'].each do |pattern| Dir.glob(pattern).each do|f| require_relative File.join(Dir.pwd, f) end end ########################################### # List specs ########################################### if 'list' == action colors = [:blue, :magenta, :yellow, :green] specs = Spectre.specs(cfg['specs'], cfg['tags']) exit 1 unless specs.any? counter = 0 specs.group_by { |x| x.subject }.each do |subject, spec_group| spec_group.each do |spec| tags = spec.tags.map { |x| '#' + x.to_s }.join ' ' desc = subject.desc desc += ' - ' + spec.context.__desc + ' -' if spec.context.__desc desc += ' ' + spec.desc puts "[#{spec.name}]".send(colors[counter % colors.length]) + " #{desc} #{tags.cyan}" end counter += 1 end exit 0 end ########################################### # Run ########################################### if 'run' == action # Initialize logger now = Time.now cfg['log_file'] = cfg['log_file'].frmt( { shortdate: now.strftime('%Y-%m-%d'), date: now.strftime('%Y-%m-%d_%H%M%S'), timestamp: now.strftime('%s'), subject: 'spectre', }) log_dir = File.dirname cfg['log_file'] FileUtils.makedirs log_dir unless Dir.exists? log_dir # Load Modules cfg['modules'] .concat(cfg['include']) .select { |mod| !cfg['exclude'].include? mod } .each do |mod| begin mod_file = mod + '.rb' spectre_lib_mod = File.join(File.dirname(__dir__), 'lib', mod_file) if File.exists? mod_file require_relative mod_file elsif File.exists? spectre_lib_mod require_relative spectre_lib_mod else require mod end rescue LoadError => e puts "Unable to load module #{mod}. Check if the module exists or remove it from your spectre config:\n#{e.message}" exit 1 end end # Load mixins cfg['mixin_patterns'].each do |pattern| Dir.glob(pattern).each do|f| require_relative File.join(Dir.pwd, f) end end Spectre.configure(cfg) Spectre::Logger.debug! if cfg['debug'] cfg['loggers'].each do |logger_name| logger = Kernel.const_get(logger_name).new(cfg) Spectre::Logger.add(logger) end if cfg['loggers'] specs = Spectre.specs(cfg['specs'], cfg['tags']) unless specs.any? puts "No specs found in #{Dir.pwd}" exit 1 end run_infos = Spectre::Runner.new.run(specs) cfg['reporters'].each do |reporter| reporter = Kernel.const_get(reporter).new(cfg) reporter.report(run_infos) end errors = run_infos.select { |x| nil != x.error or nil != x.failure } exit 0 if cfg['ignore_failure'] or not errors.any? exit 1 end ########################################### # Envs ########################################### if 'envs' == action exit 1 unless envs.any? puts envs.pretty exit 0 end ########################################### # Show ########################################### if 'show' == action puts cfg.pretty exit 0 end ########################################### # Dump ########################################### if 'dump' == action puts YAML.dump(cfg) end ########################################### # Cleanup ########################################### if 'cleanup' == action log_file_pattern = cfg['log_file'].gsub('', '*') Dir.glob(log_file_pattern).each do |log_file| File.delete(log_file) end Dir.glob(File.join cfg['out_path'], '/*').each do |out_file| File.delete(out_file) end end ########################################### # Init ########################################### DEFAULT_SPECTRE_CFG = %{log_file: ./logs/spectre_.log env_patterns: - './environments/**/*.env.yml' env_partial_patterns: - './environments/**/*.env.secret.yml' spec_patterns: - './specs/**/*.spec.rb' mixin_patterns: - '../common/**/*.mixin.rb' - './mixins/**/*.mixin.rb' resource_paths: - '../common/resources' - './resources' } DEFAULT_ENV_CFG = %{cert: &cert ./resources/.cer http: : base_url: http://localhost:5000/api/v1/ # basic_auth: # username: # password: # keystone: # url: https:///main/v3/ # username: # password: # project: # domain: # cert: *cert # ssh: # : # host: # username: # password: } DEFAULT_ENV_SECRET_CFG = %{http: : # basic_auth: # username: # password: # keystone: # username: # password: # ssh: # : # username: # password: } SAMPLE_SPEC = %[describe '' do it 'does some http requests', tags: [:sample] do log 'doing some http request' http '' do auth 'basic' # auth 'keystone' method 'GET' path 'path/to/resource' param 'id', 4295118773 param 'foo', 'bar' header 'X-Correlation-Id', '4c2367b1-bfee-4cc2-bdc5-ed17a6a9dd4b' header 'Range', 'bytes=500-999' json({ "message": "Hello Spectre!" }) end expect 'the response code to be 200' do response.code.should_be 200 end expect 'a message to exist' do response.json.message.should_not_be nil end end end ] DEFAULT_GITIGNORE = %[*.code-workspace logs/ reports/ **/environments/*.env.secret.yml ] DEFAULT_GEMFILE = %[source 'https://rubygems.org' gem 'spectre-core', '>= #{Spectre::VERSION}' # gem 'spectre-mysql', '>= 1.0.0' # gem 'spectre-ssh', '>= 1.0.0' # gem 'spectre-ftp', '>= 1.0.0' # gem 'spectre-curl', '>= 1.0.0' # gem 'spectre-git', '>= 0.1.0' ] if 'init' == action DEFAULT_FILES = [ ['./environments/default.env.yml', DEFAULT_ENV_CFG], ['./environments/default.env.secret.yml', DEFAULT_ENV_SECRET_CFG], ['./specs/sample.spec.rb', SAMPLE_SPEC], ['./spectre.yml', DEFAULT_SPECTRE_CFG], ['./.gitignore', DEFAULT_GITIGNORE], ['./Gemfile', DEFAULT_GEMFILE], ] %w(environments logs specs).each do |dir_name| Dir.mkdir(dir_name) unless File.directory? dir_name end DEFAULT_FILES.each do |file, content| unless File.exists? file File.write(file, content) end end exit 0 end