Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Expose ConfigurationOptions to Configuration #832

Closed
wants to merge 3 commits into from

5 participants

@JonRowe
Owner

Add in a way to expose configuration options into configuration, as a suggested implementation for #732

@cupakromer
Collaborator

Is there a reason to use cmdline_ as a prefix instead of exposing the option name directly? It seems the intent is to get access to some things passed in from the cli. As a user, I wouldn't care under normal circumstances if the option was initially set in the cli or some where else, as long as I had access to it's current value.

@JonRowe
Owner

Yes. It's to indicate that they are not actual Configuration options, but the options passed into the runner, they come from the command line flags or via the runner itself (in the case of automation tools). They're only really for reference use, as you can't change them, additionally, I didn't want to risk conflict with current Configuration options.

I'd be open to a different prefix, or removing the prefix entirely if @dchelimsky / @myronmarston preferred.

@dchelimsky
Owner

I like making the distinction between options defined on RSpec.configuration and those defined elsewhere, but those options can come from the command line, a file, or even an environment variable, so I like the idea of a prefix, but cmdline is a bit misleading. I don't have any good ideas but if/when I do I'll post them here.

@JonRowe
Owner

I didn't know what else to call them, but I agree that cmdline_ is potentially misleading... I was hoping someone would have a better suggestion (next time I'll mention those sort of things up front in the PR ;) )

@JonRowe
Owner

How about just option_ to indicate it's an option that's been configured elsewhere?

@myronmarston

Rather than exposing them directly off of the configuration object, what do you think about exposing a single object off of the configuration object (called something like cli_options), and then exposing each option off of that, w/o a prefix?

lib/rspec/core/configuration.rb
@@ -40,6 +40,15 @@ def #{name}
end
# @private
+ def self.define_option_reader(name)
+ eval <<-CODE
+ def cmdline_#{name}
+ option_for(#{name.inspect})
+ end
+ CODE
+ end
@myronmarston Owner

FWIW, I always prefer define_method over eval'ing a string like this. Tenderlove recently wrote up a summary comparison of the two approaches including benchmarks:

http://tenderlovemaking.com/2013/03/03/dynamic_method_definitions.html

We haven't always followed this in the rspec code base, both because we've never really talked about it as a team (and I'm not sure that others share my preference for define_method) and also because we've been forced to use the eval approach on occasion due to the fact that 1.8.6 (and earlier) didn't allow a define_method-defined method to accept a block...but moving forward, I'd like to use define_method unless we have a specific reason not to.

@JonRowe Owner
JonRowe added a note

Totally, I'd be happy to switch it, but I was following the convention of the other methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@JonRowe
Owner

I thought about that but @dchelimsky's comment in #732

I'd rather move toward ensuring that all of the options are exposed on the Configuration than
exposing another object.

convinced me otherwise.

@myronmarston

Sounds like I need to read more of the background and not just be a drive-by commenter :).

@myronmarston

@dchelimsky -- can you speak to your reasons for the statement @JonRowe quoted above? I prefer a separate object; to me, programmatically accessing the passed CLI options has quite different semantics from normal RSpec::Configuration options -- for example, they are read-only (because you can't change what the passed CLI options were once the program has begun...). I also feel like it's a cleaner approach then a common method prefix--in fact, the discussion of the method prefix is what suggested to me that we need a separate object for this. I'm sure you have totally valid reasons for the preference you expressed before but I can't think of any reasons to favor that approach at the moment.

@dchelimsky
Owner

Several (but not all) of the options can be set in a pre-process scope (e.g. external files like .rspec and/or ENV vars), and in RSpec.configure. I think it would add confusion to expose to end users e.g. RSpec.configuration.fail_fast and RSpec.configuration.pre_process_options.fail_fast.

@cupakromer
Collaborator

@dchelimsky that eloquently put into words what I was thinking in my first comment.

I read the initial issue as the need to have access to some of these read only settings (no matter where they were set). However, if there is a perfectly go alternative already in RSpec.configuration, say the list of tests to run, then use what is already in RSpec.configuration which has already been properly processed for consumption.

@JonRowe
Owner

Ok, so which should we disregard? Is there harm in exposing these under a prefix for reference purposes?

@myronmarston

I think it would add confusion to expose to end users e.g. RSpec.configuration.fail_fast and RSpec.configuration.pre_process_options.fail_fast.

But how is that any less confusing than RSpec.configuration.fail_fast and RSpec.configuration.pre_process_options.fail_fast? I don't see how a prefix is any better in this regard to their being separate object.

Thinking about this some more...I think it's pretty rare that a user needs to access the command line options (consider that #732 is the first time that a user has ever requested it, as far as I know). The approaches we're talking about here add some maintenance cost in that every time we add a new command line option, we'd have to make sure there's a corresponding method added to RSpec::Configuration (or the the object returned by RSpec::Configuration#cli_options if we go that route). Given the rarity of this need, I'd prefer not to add to our maintenance cost. I think a better approach, which doesn't add to our ongoing maintenance cost, and also removes the potential confusion of having multiple config methods named the same thing, is to have RSpec::Configuration#cli_options returned the unprocessed options using a ruby primitive (either a raw string or a hash). The end user code that consumes the options could then be something like if RSpec.configuration.cli_options.include?('--profile').... This has a nice side benefit of not requiring the user to reference API docs to see which command line flags correspond to which methods on a config object; The --some-random-option CLI option would come through in the returned object as --some-random-option -- no translation or API really needed!

@dchelimsky
Owner

Me: I think it would add confusion to expose to end users e.g. RSpec.configuration.fail_fast and RSpec.configuration.pre_process_options.fail_fast.

You: But how is that any less confusing than RSpec.configuration.fail_fast and RSpec.configuration.pre_process_options.fail_fast?

Me: Huh?

re: exposing CLI args via cli_options, what does RSpec.configuration.cli_options.include?('--color') return when .rspec contains --color and the CLI contains --no-color?

@myronmarston

Me: Huh?

Sorry, I completely failed to make my point.

The point I was trying to make is that while you are correct that having both RSpec.configuration.cli_options.fail_fast and RSpec.configuration.fail_fast could be confusing to an end user, I think it would be equally confusing to have both RSpec.configuration.cli_options_fail_fast and RSpec.configuration.fail_fast. IMO, having either a method prefix or an "object prefix" (for lack of a better term) both have the potential to cause equal confusion...but I find the "object prefix" solution to have higher cohesion. These CLI options belong together, and it seems odd (to me) to mix them into another object, but then try to differentiate them by using a common method prefix.

re: exposing CLI args via cli_options, what does RSpec.configuration.cli_options.include?('--color') return when .rspec contains --color and the CLI contains --no-color?

I've been thinking this would only be the options passed at the command line, so it would be --no-color. As I see it, the options go through multiple processing phases:

  • It starts with the raw CLI options
  • This gets merged with .rspec options
  • These get applied to the RSpec.configuration object, and the user can override them further.

RSpec.configuration contains the fully-processed current configuration options. I can see the usefulness of seeing exactly what got passed at the command line (e.g. before any more processing is done), and having access to the fully processed configuration options is quite useful, but it seems odd to me to have this feature return the partially-processed config options created from the first two phases.

If nothing else, this conversation reveals that there are some semantics of this feature that we need to nail down.

@dchelimsky
Owner

As I see it, the options go through multiple processing phases:

It starts with the raw CLI options
This gets merged with .rspec options
These get applied to the RSpec.configuration object, and the user can override them further.

It's more like this:

  • ENV["SPEC_OPTS"] options get merged over
  • CLI options, which get merged over
  • if custom options file
    • custom options file, which get merged over
  • else
    • ./.rspec-local (not in VCS), which get merged over
    • ./.rspec (in VCS), which get merged over
    • ~/.rspec (not in VCS), which get merged over
  • RSpec.configure options

See https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/configuration_options.rb#L76-L82

That the ENV options take precedence over the CLI is a legacy support issue: #276

In terms of the issue we're trying to address, if a user wishes to modify options in RSpec.configure based on CLI options, he/she is just as likely to want to be able to drive that from a user-specific options file (project-local or global), so I think exposing options at any level beyond the final merge is not only opening a rather ugly can of worms, it would also limit our options to simplify things down the road.

If, in spite of that, there is a decision to press forward, I think we should go all in and offer up an ordered source map e.g.

RSpec.configuration.source_for :color
# => [ [ ".rspec", "--no-color" ], [ "/Users/david/.rspec", "--color" ], [ "RSpec.configuration", "true" ] ]

# This would be nicer as an ordered hash, but we'd have to employ special handling for Ruby 1.8 e.g. `ActiveSupport::OrderedHash`.

I could see that being useful as a debugging tool when trying to understand option-input precedence, but how often does that need come up?

@myronmarston

Thanks for writing that up, @dchelimsky -- the config system is one of the parts of rspec I've worked in the least. I had forgotten how man layers it has.

The ordered source map idea looks nice, but it sounds like a lot of effort for something that would be rarely used.

Regardless of what direction we go (or if we do anything at all), I want to make sure that we actually wind up addressing @jasonkarns' issue he was dealing with in #732. (Jason, we'd value your feedback here!)

Revisiting my earlier idea to expose the CLI args string (which was clearly naive), would it work to expose the merged options as a hash?

@JonRowe
Owner

@myronmarston did you check out #834 ? I feel it might be a better appraoch to solving the simple cases like @jasonkarns' #732 without getting into the complexity @dchelimsky suggested, which I feel might be overkill...

@dchelimsky
Owner

FWIW I agree it would be complete overkill :) I just think dipping a toe into a deep river makes no sense either.

I think @JonRowe's solution in #834 is the right way to go here because it solves the problem without introducing a new concept.

@jasonkarns

I also like @JonRowe's solution in #834. That solves my initial issue easily. Originally, all I had wanted to do was to set our Logger level to DEBUG when RSpec was in debugging mode. It didn't matter to me whether RSpec debug mode was triggered via ENV, CLI, or .rspec file. All I was interested in was the final option state after all option parsing/merging was complete. I would hazard a guess that this hits the 80% use-case anytime one wants access to a configuration option.

@myronmarston

Sounds good. Let's move our discussion to #834. @JonRowe -- I'm heading to bed but will try to do a code review of it tomorrow.

@myronmarston myronmarston referenced this pull request from a commit
@JonRowe JonRowe alternate exposing of config variables
Conflicts:
	lib/rspec/core/configuration.rb
08fef0d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  lib/rspec/core/command_line.rb
@@ -8,6 +8,7 @@ def initialize(options, configuration=RSpec::configuration, world=RSpec::world)
end
@options = options
@configuration = configuration
+ @configuration.load_options options.parse_options
@world = world
end
View
27 lib/rspec/core/configuration.rb
@@ -40,6 +40,11 @@ def #{name}
end
# @private
+ def self.define_option_reader(name)
+ define_method("configured_option_#{name}") { option_for name }
+ end
+
+ # @private
def self.deprecate_alias_key
RSpec.warn_deprecation <<-MESSAGE
The :alias option to add_setting is deprecated. Use :alias_with on the original setting instead.
@@ -171,6 +176,17 @@ def self.add_setting(name, opts={})
# end
add_setting :treat_symbols_as_metadata_keys_with_true_values
+ # Allow access to the configuration options that RSpec was started with
+ define_option_reader :debug
+ define_option_reader :requires
+ define_option_reader :libs
+ define_option_reader :drb
+ define_option_reader :line_numbers
+ define_option_reader :full_backtrace
+ define_option_reader :profile
+ define_option_reader :files_or_directories_to_run
+ define_option_reader :full_description
+
# @private
add_setting :tty
# @private
@@ -212,6 +228,13 @@ def initialize
@fixed_color = :blue
@detail_color = :cyan
@profile_examples = false
+ @configuration_options = {}
+ end
+
+ # @private
+ # Used to load in configuration options
+ def load_options(options)
+ @configuration_options = options
end
# @private
@@ -962,6 +985,10 @@ def value_for(key, default=nil)
@preferred_options.has_key?(key) ? @preferred_options[key] : default
end
+ def option_for(key)
+ @configuration_options.fetch(key)
+ end
+
def assert_no_example_groups_defined(config_option)
if RSpec.world.example_groups.any?
raise MustBeConfiguredBeforeExampleGroupsError.new(
View
6 spec/rspec/core/command_line_spec.rb
@@ -36,6 +36,12 @@ module RSpec::Core
expect(command_line.instance_eval { @options }).to be(config_options)
end
+ it "loads submitted ConfigurationOptions into @configuration" do
+ config_options = ConfigurationOptions.new(%w[--color])
+ config.should_receive(:load_options).with(config_options.parse_options)
+ CommandLine.new(config_options)
+ end
+
describe "#run" do
context "running files" do
include_context "spec files"
View
23 spec/rspec/core/configuration_spec.rb
@@ -1372,5 +1372,28 @@ def metadata_hash(*args)
expect(groups.ordered).to eq([4, 3, 2, 1])
end
end
+
+ describe 'accessing command line options' do
+ let(:options) do
+ {
+ :debug => 'debug', :requires => 'requires', :libs => 'libs', :profile => 'profile',
+ :drb => 'drb', :files_or_directories_to_run => 'files_or_directories_to_run',
+ :line_numbers => 'line_numbers', :full_description => 'full_description',
+ :full_backtrace => 'full_backtrace'
+ }
+ end
+ before do
+ config.load_options options
+ end
+ specify { expect(config.configured_option_debug).to eq 'debug' }
+ specify { expect(config.configured_option_requires).to eq 'requires' }
+ specify { expect(config.configured_option_libs).to eq 'libs' }
+ specify { expect(config.configured_option_profile).to eq 'profile' }
+ specify { expect(config.configured_option_drb).to eq 'drb' }
+ specify { expect(config.configured_option_files_or_directories_to_run).to eq 'files_or_directories_to_run' }
+ specify { expect(config.configured_option_line_numbers).to eq 'line_numbers' }
+ specify { expect(config.configured_option_full_description).to eq 'full_description' }
+ specify { expect(config.configured_option_full_backtrace).to eq 'full_backtrace' }
+ end
end
end
Something went wrong with that request. Please try again.