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

Commit 5e395e2

Browse files
authored
Merge pull request #2538 from Mange/xdg-config-home
Add support for XDG base directory support for configuration file
2 parents 17485de + 016f336 commit 5e395e2

File tree

7 files changed

+174
-48
lines changed

7 files changed

+174
-48
lines changed

Filtering.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ focus them.
114114

115115
## Options files and command line overrides
116116

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

122122
``` ruby
123123
RSpec.describe Something, :slow => true do

features/configuration/default_path.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Feature: Setting the default spec path
55
This is supported by a `--default-path` option, which is set to `spec` by
66
default. If you prefer to keep your specs in a different directory, or assign
77
an individual file to `--default-path`, you can do so on the command line or
8-
in a configuration file (`.rspec`, `~/.rspec`, or a custom file).
8+
in a configuration file (for example `.rspec`).
99

1010
**NOTE:** this option is not supported on `RSpec.configuration`, as it needs to be
1111
set before spec files are loaded.

features/configuration/read_options_from_file.feature

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
Feature: read command line configuration options from files
22

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

6-
* Local: `./.rspec-local` (i.e. in the project's root directory, can be
7-
gitignored)
7+
The locations are:
8+
9+
* **Global options:** First file from the following list (i.e. the user's
10+
personal global options)
11+
12+
* `$XDG_CONFIG_HOME/rspec/options` ([XDG Base Directory
13+
Specification](https://specifications.freedesktop.org/basedir-spec/latest/)
14+
config)
15+
* `~/.rspec`
816

9-
* Project: `./.rspec` (i.e. in the project's root directory, usually
17+
* **Project options:** `./.rspec` (i.e. in the project's root directory, usually
1018
checked into the project)
1119

12-
* Global: `~/.rspec` (i.e. in the user's home directory)
20+
* **Local:** `./.rspec-local` (i.e. in the project's root directory, can be
21+
gitignored)
22+
23+
Options specified at the command-line has even higher specificity, as does
24+
the `SPEC_OPTS` environment variable. That means that a command-line option
25+
would overwrite a project-specific option, which overrides the global value
26+
of that option.
1327

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

1931
Scenario: Color set in `.rspec`
2032
Given a file named ".rspec" with:

lib/rspec/core/configuration.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,24 @@ module Core
99

1010
# Stores runtime configuration information.
1111
#
12-
# Configuration options are loaded from `~/.rspec`, `.rspec`,
13-
# `.rspec-local`, command line switches, and the `SPEC_OPTS` environment
14-
# variable (listed in lowest to highest precedence; for example, an option
15-
# in `~/.rspec` can be overridden by an option in `.rspec-local`).
12+
# Configuration options are loaded from multiple files and joined together
13+
# with command-line switches and the `SPEC_OPTS` environment variable.
14+
#
15+
# Precedence order (where later entries overwrite earlier entries on
16+
# conflicts):
17+
#
18+
# * Global (`$XDG_CONFIG_HOME/rspec/options`, or `~/.rspec` if it does
19+
# not exist)
20+
# * Project-specific (`./.rspec`)
21+
# * Local (`./.rspec-local`)
22+
# * Command-line options
23+
# * `SPEC_OPTS`
24+
#
25+
# For example, an option set in the local file will override an option set
26+
# in your global file.
27+
#
28+
# The global, project-specific and local files can all be overridden with a
29+
# separate custom file using the --options command-line parameter.
1630
#
1731
# @example Standard settings
1832
# RSpec.configure do |c|

lib/rspec/core/configuration_options.rb

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
module RSpec
55
module Core
66
# Responsible for utilizing externally provided configuration options,
7-
# whether via the command line, `.rspec`, `~/.rspec`, `.rspec-local`
8-
# or a custom options file.
7+
# whether via the command line, `.rspec`, `~/.rspec`,
8+
# `$XDG_CONFIG_HOME/rspec/options`, `.rspec-local` or a custom options
9+
# file.
910
class ConfigurationOptions
1011
# @param args [Array<String>] command line arguments
1112
def initialize(args)
@@ -118,7 +119,11 @@ def load_formatters_into(config)
118119
end
119120

120121
def file_options
121-
custom_options_file ? [custom_options] : [global_options, project_options, local_options]
122+
if custom_options_file
123+
[custom_options]
124+
else
125+
[global_options, project_options, local_options]
126+
end
122127
end
123128

124129
def env_options
@@ -188,13 +193,41 @@ def local_options_file
188193
end
189194

190195
def global_options_file
196+
xdg_options_file_if_exists || home_options_file_path
197+
end
198+
199+
def xdg_options_file_if_exists
200+
path = xdg_options_file_path
201+
if path && File.exist?(path)
202+
path
203+
end
204+
end
205+
206+
def home_options_file_path
191207
File.join(File.expand_path("~"), ".rspec")
192208
rescue ArgumentError
193209
# :nocov:
194210
RSpec.warning "Unable to find ~/.rspec because the HOME environment variable is not set"
195211
nil
196212
# :nocov:
197213
end
214+
215+
def xdg_options_file_path
216+
xdg_config_home = resolve_xdg_config_home
217+
if xdg_config_home
218+
File.join(xdg_config_home, "rspec", "options")
219+
end
220+
end
221+
222+
def resolve_xdg_config_home
223+
File.expand_path(ENV.fetch("XDG_CONFIG_HOME", "~/.config"))
224+
rescue ArgumentError
225+
# :nocov:
226+
# On Ruby 2.4, `File.expand("~")` works even if `ENV['HOME']` is not set.
227+
# But on earlier versions, it fails.
228+
nil
229+
# :nocov:
230+
end
198231
end
199232
end
200233
end

spec/rspec/core/configuration_options_spec.rb

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
412412
end
413413
end
414414

415+
context "defined in $XDG_CONFIG_HOME/rspec/options" do
416+
it "mentions the file name in the error so users know where to look for it" do
417+
file_name = File.expand_path("~/.config/rspec/options")
418+
create_fixture_file(file_name, "--foo_bar")
419+
expect_parsing_to_fail_mentioning_source(file_name)
420+
end
421+
end
422+
415423
context "defined in SPEC_OPTS" do
416424
it "mentions ENV['SPEC_OPTS'] as the source in the error so users know where to look for it" do
417425
with_env_vars 'SPEC_OPTS' => "--foo_bar" do
@@ -440,25 +448,64 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
440448
end
441449
end
442450

443-
describe "sources: ~/.rspec, ./.rspec, ./.rspec-local, custom, CLI, and SPEC_OPTS" do
444-
it "merges global, local, SPEC_OPTS, and CLI" do
445-
File.open("./.rspec", "w") {|f| f << "--require some_file"}
446-
File.open("./.rspec-local", "w") {|f| f << "--format global"}
447-
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--force-color"}
451+
describe "sources: $XDG_CONFIG_HOME/rspec/options, ~/.rspec, ./.rspec, ./.rspec-local, custom, CLI, and SPEC_OPTS" do
452+
it "merges both global, local, SPEC_OPTS, and CLI" do
453+
create_fixture_file("./.rspec", "--require some_file")
454+
create_fixture_file("./.rspec-local", "--format global")
455+
create_fixture_file("~/.rspec", "--force-color")
456+
create_fixture_file("~/.config/rspec/options", "--order defined")
448457
with_env_vars 'SPEC_OPTS' => "--example 'foo bar'" do
449458
options = parse_options("--drb")
450-
expect(options[:color_mode]).to eq(:on)
459+
# $XDG_CONFIG_HOME/rspec/options file ("order") is read, but ~/.rspec
460+
# file ("color") is not read because ~/.rspec has lower priority over
461+
# the file in the XDG config directory.
462+
expect(options[:order]).to eq("defined")
463+
expect(options[:color_mode]).to be_nil
464+
451465
expect(options[:requires]).to eq(["some_file"])
452466
expect(options[:full_description]).to eq([/foo\ bar/])
453467
expect(options[:drb]).to be_truthy
454468
expect(options[:formatters]).to eq([['global']])
455469
end
456470
end
457471

472+
it "reads ~/.rspec if $XDG_CONFIG_HOME/rspec/options is not found" do
473+
create_fixture_file("~/.rspec", "--force-color")
474+
475+
options = parse_options()
476+
expect(options[:color_mode]).to eq(:on)
477+
expect(options[:order]).to be_nil
478+
end
479+
480+
it "does not read ~/.rspec if $XDG_CONFIG_HOME/rspec/options is present" do
481+
create_fixture_file("~/.rspec", "--force-color")
482+
create_fixture_file("~/.config/rspec/options", "--order defined")
483+
484+
options = parse_options()
485+
expect(options[:color_mode]).to be_nil
486+
expect(options[:order]).to eq("defined")
487+
end
488+
489+
it "uses $XDG_CONFIG_HOME environment variable when set to find XDG global options" do
490+
create_fixture_file("~/.config/rspec/options", "--format default_xdg")
491+
create_fixture_file("~/.custom-config/rspec/options", "--format overridden_xdg")
492+
493+
with_env_vars 'XDG_CONFIG_HOME' => "~/.custom-config" do
494+
options = parse_options()
495+
expect(options[:formatters]).to eq([['overridden_xdg']])
496+
end
497+
498+
without_env_vars 'XDG_CONFIG_HOME' do
499+
options = parse_options()
500+
expect(options[:formatters]).to eq([['default_xdg']])
501+
end
502+
end
503+
458504
it 'ignores file or dir names put in one of the option files or in SPEC_OPTS, since those are for persistent options' do
459-
File.open("./.rspec", "w") { |f| f << "path/to/spec_1.rb" }
460-
File.open("./.rspec-local", "w") { |f| f << "path/to/spec_2.rb" }
461-
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "path/to/spec_3.rb"}
505+
create_fixture_file("./.rspec", "path/to/spec_1.rb" )
506+
create_fixture_file("./.rspec-local", "path/to/spec_2.rb" )
507+
create_fixture_file("~/.rspec", "path/to/spec_3.rb")
508+
create_fixture_file("~/.config/rspec/options", "path/to/spec_4.rb")
462509
with_env_vars 'SPEC_OPTS' => "path/to/spec_4.rb" do
463510
options = parse_options()
464511
expect(options[:files_or_directories_to_run]).to eq([])
@@ -472,13 +519,14 @@ def expect_parsing_to_fail_mentioning_source(source, options=[])
472519
end
473520

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

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

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

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

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

511560
context "with custom options file" do
512561
it "ignores project and global options files" do
513-
File.open("./.rspec", "w") {|f| f << "--format project"}
514-
File.open(File.expand_path("~/.rspec"), "w") {|f| f << "--format global"}
515-
File.open("./custom.opts", "w") {|f| f << "--force-color"}
562+
create_fixture_file("./.rspec", "--format project")
563+
create_fixture_file("~/.rspec", "--format global")
564+
create_fixture_file("~/.config/rspec/options", "--format xdg")
565+
create_fixture_file("./custom.opts", "--force-color")
516566
options = parse_options("-O", "./custom.opts")
517567
expect(options[:format]).to be_nil
518568
expect(options[:color_mode]).to eq(:on)
519569
end
520570

521571
it "parses -e 'full spec description'" do
522-
File.open("./custom.opts", "w") {|f| f << "-e 'The quick brown fox jumps over the lazy dog'"}
572+
create_fixture_file("./custom.opts", "-e 'The quick brown fox jumps over the lazy dog'")
523573
options = parse_options("-O", "./custom.opts")
524574
expect(options[:full_description]).to eq([/The\ quick\ brown\ fox\ jumps\ over\ the\ lazy\ dog/])
525575
end
Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
require 'tmpdir'
22
require 'fileutils'
3+
require 'pathname'
34

45
RSpec.shared_context "isolated home directory" do
56
around do |ex|
67
Dir.mktmpdir do |tmp_dir|
7-
original_home = ENV['HOME']
8-
begin
9-
ENV['HOME'] = tmp_dir
10-
ex.call
11-
ensure
12-
ENV['HOME'] = original_home
8+
# If user running this test suite has a custom $XDG_CONFIG_HOME, also
9+
# clear that out when changing $HOME so tests don't touch the user's real
10+
# configuration files.
11+
without_env_vars "XDG_CONFIG_HOME" do
12+
with_env_vars "HOME" => tmp_dir do
13+
ex.call
14+
end
1315
end
1416
end
1517
end
1618
end
1719

20+
module HomeFixtureHelpers
21+
def create_fixture_file(file_name, contents)
22+
path = Pathname.new(file_name).expand_path
23+
if !path.exist?
24+
path.dirname.mkpath
25+
# Pathname#write does not exist in all supported Ruby versions
26+
File.open(path.to_s, 'w') { |file| file << contents }
27+
else
28+
# Abort just in case we're about to destroy something important.
29+
raise "File at #{path} already exists!"
30+
end
31+
end
32+
end
33+
1834
RSpec.configure do |c|
1935
c.include_context "isolated home directory", :isolated_home => true
36+
c.include HomeFixtureHelpers, :isolated_home => true
2037
end

0 commit comments

Comments
 (0)