lib/rspec/core/configuration_options.rb
require 'erb'
require 'shellwords'
module RSpec
module Core
# Responsible for utilizing externally provided configuration options,
# whether via the command line, `.rspec`, `~/.rspec`,
# `$XDG_CONFIG_HOME/rspec/options`, `.rspec-local` or a custom options
# file.
class ConfigurationOptions
# @param args [Array<String>] command line arguments
def initialize(args)
@args = args.dup
organize_options
end
# Updates the provided {Configuration} instance based on the provided
# external configuration options.
#
# @param config [Configuration] the configuration instance to update
def configure(config)
process_options_into config
configure_filter_manager config.filter_manager
load_formatters_into config
end
# @api private
# Updates the provided {FilterManager} based on the filter options.
# @param filter_manager [FilterManager] instance to update
def configure_filter_manager(filter_manager)
@filter_manager_options.each do |command, value|
filter_manager.__send__ command, value
end
end
# @return [Hash] the final merged options, drawn from all external sources
attr_reader :options
# @return [Array<String>] the original command-line arguments
attr_reader :args
private
def organize_options
@filter_manager_options = []
@options = (file_options << command_line_options << env_options).each do |opts|
@filter_manager_options << [:include, opts.delete(:inclusion_filter)] if opts.key?(:inclusion_filter)
@filter_manager_options << [:exclude, opts.delete(:exclusion_filter)] if opts.key?(:exclusion_filter)
end
@options = @options.inject(:libs => [], :requires => []) do |hash, opts|
hash.merge(opts) do |key, oldval, newval|
[:libs, :requires].include?(key) ? oldval + newval : newval
end
end
end
UNFORCED_OPTIONS = Set.new([
:requires, :profile, :drb, :libs, :files_or_directories_to_run,
:full_description, :full_backtrace, :tty
])
UNPROCESSABLE_OPTIONS = Set.new([:formatters])
def force?(key)
!UNFORCED_OPTIONS.include?(key)
end
def order(keys)
OPTIONS_ORDER.reverse_each do |key|
keys.unshift(key) if keys.delete(key)
end
keys
end
OPTIONS_ORDER = [
# It's important to set this before anything that might issue a
# deprecation (or otherwise access the reporter).
:deprecation_stream,
# load paths depend on nothing, but must be set before `requires`
# to support load-path-relative requires.
:libs,
# `files_or_directories_to_run` uses `default_path` so it must be
# set before it.
:default_path, :only_failures,
# These must be set before `requires` to support checking
# `config.files_to_run` from within `spec_helper.rb` when a
# `-rspec_helper` option is used.
:files_or_directories_to_run, :pattern, :exclude_pattern,
# Necessary so that the `--seed` option is applied before requires,
# in case required files do something with the provided seed.
# (such as seed global randomization with it).
:order,
# In general, we want to require the specified files as early as
# possible. The `--require` option is specifically intended to allow
# early requires. For later requires, they can just put the require in
# their spec files, but `--require` provides a unique opportunity for
# users to instruct RSpec to load an extension file early for maximum
# flexibility.
:requires
]
def process_options_into(config)
opts = options.reject { |k, _| UNPROCESSABLE_OPTIONS.include? k }
order(opts.keys).each do |key|
force?(key) ? config.force(key => opts[key]) : config.__send__("#{key}=", opts[key])
end
end
def load_formatters_into(config)
options[:formatters].each { |pair| config.add_formatter(*pair) } if options[:formatters]
end
def file_options
if custom_options_file
[custom_options]
else
[global_options, project_options, local_options]
end
end
def env_options
return {} unless ENV['SPEC_OPTS']
parse_args_ignoring_files_or_dirs_to_run(
Shellwords.split(ENV["SPEC_OPTS"]),
"ENV['SPEC_OPTS']"
)
end
def command_line_options
@command_line_options ||= Parser.parse(@args)
end
def custom_options
options_from(custom_options_file)
end
def local_options
@local_options ||= options_from(local_options_file)
end
def project_options
@project_options ||= options_from(project_options_file)
end
def global_options
@global_options ||= options_from(global_options_file)
end
def options_from(path)
args = args_from_options_file(path)
parse_args_ignoring_files_or_dirs_to_run(args, path)
end
def parse_args_ignoring_files_or_dirs_to_run(args, source)
options = Parser.parse(args, source)
options.delete(:files_or_directories_to_run)
options
end
def args_from_options_file(path)
return [] unless path && File.exist?(path)
config_string = options_file_as_erb_string(path)
FlatMap.flat_map(config_string.split(/\n+/), &:shellsplit)
end
def options_file_as_erb_string(path)
if RUBY_VERSION >= '2.6'
ERB.new(File.read(path), :trim_mode => '-').result(binding)
else
ERB.new(File.read(path), nil, '-').result(binding)
end
end
def custom_options_file
command_line_options[:custom_options_file]
end
def project_options_file
"./.rspec"
end
def local_options_file
"./.rspec-local"
end
def global_options_file
xdg_options_file_if_exists || home_options_file_path
end
def xdg_options_file_if_exists
path = xdg_options_file_path
if path && File.exist?(path)
path
end
end
def home_options_file_path
File.join(File.expand_path("~"), ".rspec")
rescue ArgumentError
# :nocov:
RSpec.warning "Unable to find ~/.rspec because the HOME environment variable is not set"
nil
# :nocov:
end
def xdg_options_file_path
xdg_config_home = resolve_xdg_config_home
if xdg_config_home
File.join(xdg_config_home, "rspec", "options")
end
end
def resolve_xdg_config_home
File.expand_path(ENV.fetch("XDG_CONFIG_HOME", "~/.config"))
rescue ArgumentError
# :nocov:
# On Ruby 2.4, `File.expand("~")` works even if `ENV['HOME']` is not set.
# But on earlier versions, it fails.
nil
# :nocov:
end
end
end
end