Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #2538 from Mange/xdg-config-home
Browse files Browse the repository at this point in the history
Add support for XDG base directory support for configuration file
  • Loading branch information
myronmarston authored May 9, 2018
2 parents 17485de + 016f336 commit 5e395e2
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 48 deletions.
8 changes: 4 additions & 4 deletions Filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,10 @@ focus them.

## Options files and command line overrides

Command line option declarations can be stored in `.rspec`, `~/.rspec`, or a custom
options file. This is useful for storing defaults. For example, let's
say you've got some slow specs that you want to suppress most of the
time. You can tag them like this:
Command line option declarations can be stored in `.rspec`, `~/.rspec`,
`$XDG_CONFIG_HOME/rspec/options` or a custom options file. This is useful for
storing defaults. For example, let's say you've got some slow specs that you
want to suppress most of the time. You can tag them like this:

``` ruby
RSpec.describe Something, :slow => true do
Expand Down
2 changes: 1 addition & 1 deletion features/configuration/default_path.feature
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Feature: Setting the default spec path
This is supported by a `--default-path` option, which is set to `spec` by
default. If you prefer to keep your specs in a different directory, or assign
an individual file to `--default-path`, you can do so on the command line or
in a configuration file (`.rspec`, `~/.rspec`, or a custom file).
in a configuration file (for example `.rspec`).

**NOTE:** this option is not supported on `RSpec.configuration`, as it needs to be
set before spec files are loaded.
Expand Down
32 changes: 22 additions & 10 deletions features/configuration/read_options_from_file.feature
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
Feature: read command line configuration options from files

RSpec reads command line configuration options from files in three different
locations:
RSpec reads command line configuration options from several different files,
all conforming to a specific level of specificity. Options from a higher
specificity will override conflicting options from lower specificity files.

* Local: `./.rspec-local` (i.e. in the project's root directory, can be
gitignored)
The locations are:

* **Global options:** First file from the following list (i.e. the user's
personal global options)

* `$XDG_CONFIG_HOME/rspec/options` ([XDG Base Directory
Specification](https://specifications.freedesktop.org/basedir-spec/latest/)
config)
* `~/.rspec`

* Project: `./.rspec` (i.e. in the project's root directory, usually
* **Project options:** `./.rspec` (i.e. in the project's root directory, usually
checked into the project)

* Global: `~/.rspec` (i.e. in the user's home directory)
* **Local:** `./.rspec-local` (i.e. in the project's root directory, can be
gitignored)

Options specified at the command-line has even higher specificity, as does
the `SPEC_OPTS` environment variable. That means that a command-line option
would overwrite a project-specific option, which overrides the global value
of that option.

Configuration options are loaded from `~/.rspec`, `.rspec`, `.rspec-local`,
command line switches, and the `SPEC_OPTS` environment variable (listed in
lowest to highest precedence; for example, an option in `~/.rspec` can be
overridden by an option in `.rspec-local`).
The default options files can all be ignored using the `--options`
command-line argument, which selects a custom file to load options from.

Scenario: Color set in `.rspec`
Given a file named ".rspec" with:
Expand Down
22 changes: 18 additions & 4 deletions lib/rspec/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,24 @@ module Core

# Stores runtime configuration information.
#
# Configuration options are loaded from `~/.rspec`, `.rspec`,
# `.rspec-local`, command line switches, and the `SPEC_OPTS` environment
# variable (listed in lowest to highest precedence; for example, an option
# in `~/.rspec` can be overridden by an option in `.rspec-local`).
# Configuration options are loaded from multiple files and joined together
# with command-line switches and the `SPEC_OPTS` environment variable.
#
# Precedence order (where later entries overwrite earlier entries on
# conflicts):
#
# * Global (`$XDG_CONFIG_HOME/rspec/options`, or `~/.rspec` if it does
# not exist)
# * Project-specific (`./.rspec`)
# * Local (`./.rspec-local`)
# * Command-line options
# * `SPEC_OPTS`
#
# For example, an option set in the local file will override an option set
# in your global file.
#
# The global, project-specific and local files can all be overridden with a
# separate custom file using the --options command-line parameter.
#
# @example Standard settings
# RSpec.configure do |c|
Expand Down
39 changes: 36 additions & 3 deletions lib/rspec/core/configuration_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
module RSpec
module Core
# Responsible for utilizing externally provided configuration options,
# whether via the command line, `.rspec`, `~/.rspec`, `.rspec-local`
# or a custom options file.
# 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)
Expand Down Expand Up @@ -118,7 +119,11 @@ def load_formatters_into(config)
end

def file_options
custom_options_file ? [custom_options] : [global_options, project_options, local_options]
if custom_options_file
[custom_options]
else
[global_options, project_options, local_options]
end
end

def env_options
Expand Down Expand Up @@ -188,13 +193,41 @@ def local_options_file
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
90 changes: 70 additions & 20 deletions spec/rspec/core/configuration_options_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
end
end

context "defined in $XDG_CONFIG_HOME/rspec/options" do
it "mentions the file name in the error so users know where to look for it" do
file_name = File.expand_path("~/.config/rspec/options")
create_fixture_file(file_name, "--foo_bar")
expect_parsing_to_fail_mentioning_source(file_name)
end
end

context "defined in SPEC_OPTS" do
it "mentions ENV['SPEC_OPTS'] as the source in the error so users know where to look for it" do
with_env_vars 'SPEC_OPTS' => "--foo_bar" do
Expand Down Expand Up @@ -440,25 +448,64 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
end
end

describe "sources: ~/.rspec, ./.rspec, ./.rspec-local, custom, CLI, and SPEC_OPTS" do
it "merges global, local, SPEC_OPTS, and CLI" do
File.open("./.rspec", "w") {|f| f << "--require some_file"}
File.open("./.rspec-local", "w") {|f| f << "--format global"}
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--force-color"}
describe "sources: $XDG_CONFIG_HOME/rspec/options, ~/.rspec, ./.rspec, ./.rspec-local, custom, CLI, and SPEC_OPTS" do
it "merges both global, local, SPEC_OPTS, and CLI" do
create_fixture_file("./.rspec", "--require some_file")
create_fixture_file("./.rspec-local", "--format global")
create_fixture_file("~/.rspec", "--force-color")
create_fixture_file("~/.config/rspec/options", "--order defined")
with_env_vars 'SPEC_OPTS' => "--example 'foo bar'" do
options = parse_options("--drb")
expect(options[:color_mode]).to eq(:on)
# $XDG_CONFIG_HOME/rspec/options file ("order") is read, but ~/.rspec
# file ("color") is not read because ~/.rspec has lower priority over
# the file in the XDG config directory.
expect(options[:order]).to eq("defined")
expect(options[:color_mode]).to be_nil

expect(options[:requires]).to eq(["some_file"])
expect(options[:full_description]).to eq([/foo\ bar/])
expect(options[:drb]).to be_truthy
expect(options[:formatters]).to eq([['global']])
end
end

it "reads ~/.rspec if $XDG_CONFIG_HOME/rspec/options is not found" do
create_fixture_file("~/.rspec", "--force-color")

options = parse_options()
expect(options[:color_mode]).to eq(:on)
expect(options[:order]).to be_nil
end

it "does not read ~/.rspec if $XDG_CONFIG_HOME/rspec/options is present" do
create_fixture_file("~/.rspec", "--force-color")
create_fixture_file("~/.config/rspec/options", "--order defined")

options = parse_options()
expect(options[:color_mode]).to be_nil
expect(options[:order]).to eq("defined")
end

it "uses $XDG_CONFIG_HOME environment variable when set to find XDG global options" do
create_fixture_file("~/.config/rspec/options", "--format default_xdg")
create_fixture_file("~/.custom-config/rspec/options", "--format overridden_xdg")

with_env_vars 'XDG_CONFIG_HOME' => "~/.custom-config" do
options = parse_options()
expect(options[:formatters]).to eq([['overridden_xdg']])
end

without_env_vars 'XDG_CONFIG_HOME' do
options = parse_options()
expect(options[:formatters]).to eq([['default_xdg']])
end
end

it 'ignores file or dir names put in one of the option files or in SPEC_OPTS, since those are for persistent options' do
File.open("./.rspec", "w") { |f| f << "path/to/spec_1.rb" }
File.open("./.rspec-local", "w") { |f| f << "path/to/spec_2.rb" }
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "path/to/spec_3.rb"}
create_fixture_file("./.rspec", "path/to/spec_1.rb" )
create_fixture_file("./.rspec-local", "path/to/spec_2.rb" )
create_fixture_file("~/.rspec", "path/to/spec_3.rb")
create_fixture_file("~/.config/rspec/options", "path/to/spec_4.rb")
with_env_vars 'SPEC_OPTS' => "path/to/spec_4.rb" do
options = parse_options()
expect(options[:files_or_directories_to_run]).to eq([])
Expand All @@ -472,13 +519,14 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
end

it "prefers CLI over file options" do
File.open("./.rspec", "w") {|f| f << "--format project"}
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--format global"}
create_fixture_file("./.rspec", "--format project")
create_fixture_file("~/.rspec", "--format global")
create_fixture_file("~/.config/rspec/options", "--format xdg")
expect(parse_options("--format", "cli")[:formatters]).to eq([['cli']])
end

it "prefers CLI over file options for filter inclusion" do
File.open("./.rspec", "w") {|f| f << "--tag ~slow"}
create_fixture_file("./.rspec", "--tag ~slow")
opts = config_options_object("--tag", "slow")
config = RSpec::Core::Configuration.new
opts.configure(config)
Expand All @@ -487,14 +535,15 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
end

it "prefers project file options over global file options" do
File.open("./.rspec", "w") {|f| f << "--format project"}
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--format global"}
create_fixture_file("./.rspec", "--format project")
create_fixture_file("~/.rspec", "--format global")
create_fixture_file("~/.config/rspec/options", "--format xdg")
expect(parse_options[:formatters]).to eq([['project']])
end

it "prefers local file options over project file options" do
File.open("./.rspec-local", "w") {|f| f << "--format local"}
File.open("./.rspec", "w") {|f| f << "--format global"}
create_fixture_file("./.rspec-local", "--format local")
create_fixture_file("./.rspec", "--format global")
expect(parse_options[:formatters]).to eq([['local']])
end

Expand All @@ -510,16 +559,17 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])

context "with custom options file" do
it "ignores project and global options files" do
File.open("./.rspec", "w") {|f| f << "--format project"}
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--format global"}
File.open("./custom.opts", "w") {|f| f << "--force-color"}
create_fixture_file("./.rspec", "--format project")
create_fixture_file("~/.rspec", "--format global")
create_fixture_file("~/.config/rspec/options", "--format xdg")
create_fixture_file("./custom.opts", "--force-color")
options = parse_options("-O", "./custom.opts")
expect(options[:format]).to be_nil
expect(options[:color_mode]).to eq(:on)
end

it "parses -e 'full spec description'" do
File.open("./custom.opts", "w") {|f| f << "-e 'The quick brown fox jumps over the lazy dog'"}
create_fixture_file("./custom.opts", "-e 'The quick brown fox jumps over the lazy dog'")
options = parse_options("-O", "./custom.opts")
expect(options[:full_description]).to eq([/The\ quick\ brown\ fox\ jumps\ over\ the\ lazy\ dog/])
end
Expand Down
29 changes: 23 additions & 6 deletions spec/support/isolated_home_directory.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
require 'tmpdir'
require 'fileutils'
require 'pathname'

RSpec.shared_context "isolated home directory" do
around do |ex|
Dir.mktmpdir do |tmp_dir|
original_home = ENV['HOME']
begin
ENV['HOME'] = tmp_dir
ex.call
ensure
ENV['HOME'] = original_home
# If user running this test suite has a custom $XDG_CONFIG_HOME, also
# clear that out when changing $HOME so tests don't touch the user's real
# configuration files.
without_env_vars "XDG_CONFIG_HOME" do
with_env_vars "HOME" => tmp_dir do
ex.call
end
end
end
end
end

module HomeFixtureHelpers
def create_fixture_file(file_name, contents)
path = Pathname.new(file_name).expand_path
if !path.exist?
path.dirname.mkpath
# Pathname#write does not exist in all supported Ruby versions
File.open(path.to_s, 'w') { |file| file << contents }
else
# Abort just in case we're about to destroy something important.
raise "File at #{path} already exists!"
end
end
end

RSpec.configure do |c|
c.include_context "isolated home directory", :isolated_home => true
c.include HomeFixtureHelpers, :isolated_home => true
end

0 comments on commit 5e395e2

Please sign in to comment.