#! /usr/bin/ruby # frozen_string_literal: true require 'yaml' require 'ostruct' require 'optparse' require 'fileutils' require 'ectoplasm' require_relative '../lib/spectre' class ::Hash def deep_merge!(second) merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge!(v2, &merger) : v2 } self.merge!(second, &merger) end end 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', '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', 'spectre/ssh', 'spectre/ftp', 'spectre/mysql', # 'spectre/postgres', ], 'include' => [ ], 'exclude' => [ ] } cmd_options = {} 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 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('-o PATH', '--out PATH', 'Output directory path') do |path| cmd_options['out_path'] = path end opts.on('-r NAME', '--reporter 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 cmd_options[key] = val curr_opt = cmd_options (key.split '.').each do |k| curr_opt[k] = {} if not curr_opt.key? k end curr_opt = val 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.delete('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 if not envs.key? name envs[name].deep_merge! partial_env end end env = envs[cfg['environment']] cfg.merge! env if env String.colored! if cfg['colored'] ########################################### # 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 action == 'list' colors = [:blue, :magenta, :yellow, :green] specs = Spectre.specs(cfg['specs'], cfg['tags']) exit 1 if specs.length == 0 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 action == 'run' # 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 if !Dir.exists? log_dir # Load Modules cfg['modules'] .concat(cfg['include']) .select { |mod| !cfg['exclude'].include? mod } .each do |mod| begin spectre_lib_mod = File.join('../lib', mod) if File.exists? mod require_relative mod 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 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']) if specs.length == 0 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 exit 0 end ########################################### # Envs ########################################### if action == 'envs' exit 1 if envs.length == 0 puts envs.pretty exit 0 end ########################################### # Show ########################################### if action == 'show' puts cfg.pretty exit 0 end ########################################### # Dump ########################################### if action == 'dump' puts YAML.dump(cfg) 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 ] if action == 'init' 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], ] %w(environments logs specs).each do |dir_name| Dir.mkdir(dir_name) unless File.directory? dir_name end DEFAULT_FILES.each do |file, content| if not File.exists? file File.write(file, content) end end exit 0 end