#! /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 = {} 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('--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 cmd_options[key] = val curr_opt = cmd_options (key.split '.').each do |k| curr_opt[k] = {} unless 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['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.merge! env if env 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 ########################################### # 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