-
-
Notifications
You must be signed in to change notification settings - Fork 753
Support bisect in spec opts #2271
Support bisect in spec opts #2271
Conversation
6ee83a5
to
c2ca932
Compare
Hey @urbanautomaton, thanks for taking a stab at this. It definitely looks like a good start. I think the refactoring of option parsing can be a self-contained PR. What do you think about opening a PR for just that so we can get it merged quick? Then you can deal with the bisect issues in a PR on top of that. I'll go leave a few comments about the option parsing stuff on this PR now as well. |
@@ -13,6 +13,10 @@ def initialize(args) | |||
organize_options | |||
end | |||
|
|||
def command_line_and_env_args |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me why we need a method containing both command line and env args but not args from the dot files. Can you explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because I was transferring all the env args to the CLI args of the child processes and removing SPEC_OPTS completely for the children, but there's actually no need to do that, so this can be removed.
c2ca932
to
68398d6
Compare
@myronmarston Thanks for the feedback. Responding inline here so the convo doesn't get lost when the diff changes:
I had a stab at this, but ran into difficulties when porting I've now tried something similar to what you suggest, but with factory methods returning lambdas that can capture any information necessary for the particular callable. What do you think? |
68398d6
to
1c5818d
Compare
@@ -89,6 +89,10 @@ def run_command(cmd) | |||
# :nocov: | |||
end | |||
|
|||
def spec_opts_without_bisect | |||
{ 'SPEC_OPTS' => ENV.fetch('SPEC_OPTS', '').gsub(/--bisect/, '') } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the user has put --bisect=verbose
in SPEC_OPTS
? This gsub will leave =verbose
.
👍 |
de0e370
to
21ed282
Compare
I've addressed the feedback so far, and replaced the cuke with assertions at the For JRuby and 1.8.7, At the moment I'm testing the runner callables as part of the The suite is almost totally green, but for a single warning generated by the 2.2 job. This job is running 2.2.0-p0, and the warning seems to be in error, as it doesn't appear in later versions of the 2.2.x branch. Is testing against 2.2.0-p0 deliberate, or could this job be updated to 2.2.5? If the former, I'd love a pointer as to how to address this warning, because I can't see anything wrong with the code that's triggering it. |
This was a warning added to 2.2.0: https://bugs.ruby-lang.org/issues/10661 Ruby-core removed the warning when I raised that issue, and it got removed from 2.2.1. (Unfortunately, it was added at the last minute to 2.2.0 and so it was never part of any of the RCs so I could notice and comment then). To fix the warning on 2.2.0, you'll need to apply one of these fixes:
All the fixes for this are sub-optimal and the warning is annoying (it's why I raised the issue on ruby-core when I first noticed).
Testing against 2.2.0-p0 is not deliberate. I thought that are use of |
expect(Open3).to have_received(open3_method).with(command) | ||
expect(called_environment).to include(environment) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If open3_method
is set to something besides capture2e
, the match
logic does nothing, which could lead to a silent failure. I think I'd prefer to see this as:
if open3_method == :capture2e
RSpec::Matchers.define :invoke_command_with_env do |command, environment|
# ...
end
elsif open3_method == :popen3
RSpec::Matchers.define :invoke_command_with_env do |command, environment|
# ...
end
end
Then, if a different method is used, the matcher won't even be defined and we'll definitely get an error in that case.
👍 Looking good, @urbanautomaton! We are expecting to release 3.5 in the next few days (we plan to release as soon as rails 5 final ships, and we hear it should be this week), so I'm hoping we can get this merged before 3.5 ships :). |
What you've done is fine. Your idea to take it a step further with |
end | ||
else # for 1.8.7 | ||
# :nocov: | ||
def run_command(cmd) | ||
out = err = nil | ||
|
||
original_spec_opts = ENV['SPEC_OPTS'] | ||
ENV['SPEC_OPTS'] = spec_opts_without_bisect | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 did you try using the open3 feature of passing in the env opts?
The parameters env, cmd, and opts are passed to Process.spawn
env: hash
name => val : set the environment variable
name => nil : unset the environment variable
options: hash
clearing environment variables:
:unsetenv_others => true : clear environment variables except specified by env
:unsetenv_others => false : don't clear (default)
If a hash is given as env, the environment is updated by env before exec(2) in the child process. If a pair in env has nil as the value, the variable is deleted.set FOO as BAR and unset BAZ.
pid = spawn({"FOO"=>"BAR", "BAZ"=>nil}, command)The :unsetenv_others key in options specifies to clear environment variables, other than specified by env.
pid = spawn(command, :unsetenv_others=>true) # no environment variable
pid = spawn({"FOO"=>"BAR"}, command, :unsetenv_others=>true) # FOO only
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The upside of supporting ruby 1.8.7 is that I get to use the old hash syntax without feeling guilt. The downside is that some of stdlib wasn't as capable back then, and Open3
/Process.spawn
is one of those bits, unfortunately. I've used the new passed environment where possible, but for 1.8.7 it's either put the new env in the command string or mutate ENV
, so I plumped for the latter.
f2db0e3
to
6baac52
Compare
Okay - given the imminent release, then, I'd like to make that change but will tackle it in a separate PR. I think the build should be passing again now (it's being very slow kicking off for some reason). I'm still not entirely happy with the |
a_string_including("--seed 1234"), | ||
{ 'SPEC_OPTS' => '--fail-fast' } | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean about it being tricky to figure out what to treat as the subject for these expectations. Maybe the block approach does make the most sense. I'm happy to merge with this has it is but if you want to switch back to the block form that would be fine, too.
I think Travis is starting it now. Must have just gotten backed up for some reason.
I left the one comment above about switching back to a block perhaps making more sense and being OK, but whichever approach you decide to take there is fine. Regardless, this LGTM. I'll be happy to merge after you've rebased into a clearer story as you said :). |
The build is green now, FWIW :). |
095aaf5
to
40154ab
Compare
Okay - I went back to the block form for the The |
|
||
supports_block_expectations | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a merge blocker, but I think maybe the matcher should be named invoke_command_with_env
instead of the past-tense have_invoked_command_with_env
. Consider that all our other block matchers are named with the present tense form (e.g. change
over have_changed
, raise_error
over have_raised_error
, etc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh - yeah, that's an artifact of switching to the non-block style and back again, will change it now.
Currently if RSpec is invoked with SPEC_OPTS="--bisect", an infinite number of child processes is spawned. This is because the bisect feature works by spawning child processes for each bisection run. In the normal cli usage case, the --bisect argument is removed before the child processes are invoked. It is not removed from the SPEC_OPTS variable, however, meaning each child process believes it's starting a new bisection. This commit allows Bisect::Runner to invoke its child processes with a modified environment, removing --bisect if present. In modern rubies we can use Open3.capture2e's optional environment argument - in older rubies and JRuby we have to fall back on modifying the parent process's environment, then restoring it after the child process has run. Previously the Bisect::Runner was invoked during options parsing. However, because SPEC_OPTS and the cli args are parsed by separate instances of the parser, invoking the bisection during SPEC_OPTS parsing meant the cli args were lost. To address this I've made the OptionParser set a :bisect option, which is then picked up in the main Runner to invoke the actual bisection. This has increased the complexity of Runner.run, and there are other invocations remaining in the OptionParser (e.g. --init and --help). These will be refactored in a follow-up commit, so that the OptionParser sets a callable in each case that can be called in the main Runner.
RSpec can be invoked with a number of CLI flags that result in substantially different code paths. As well as the default runner, there is: * `--init` - initializes the project directory and exits * `--help` - prints CLI usage and exits * `--version` - prints the version and exits * `--drb` - runs examples via a DRb connection * `--bisect` - starts a bisection process Previously some of these paths were invoked directly in the OptionParser (--init, --help and --version), while others were invoked by the main Runner.run call. This necessitated a lot of complexity in Runner.run, and a reasonable amount of stubbing of exit calls (which can be error-prone, and lead to a prematurely-terminated test run). This commit refactors the different invocations into callables, which the OptionParser sets as the `:runner` option. If an alternative invocation is found in Runner.run, it is used, falling back to the default of instantiating a Runner and starting it. The invocation callables do not handle exiting themselves, but are required to return an exit code instead. They must all accept the following arguments: @param options [RSpec::Core::ConfigurationOptions] @param err [IO] @param out [IO] At the moment the invocations are tested via the OptionParser specs - in future work we intend to extract these to their own module and test them independently.
This method is now occasionally (but not reliably) failing the PerceivedComplexity cop. Since the complexity of this method is hard to control and the CyclomaticComplexity rule is already disabled for it, I'm turning off the cop for this method only.
Previously, this was set to `2.2` in the expectation that travis would use the latest of the 2.2.x branch as a result. However, builds appear to be running with 2.2.0-p0, which contains erroneous warnings that can force variables and lets to be needlessly renamed. While this may be a travis bug, I'm explicitly setting the version in the configuration to ensure that an up-to-date version is picked up in the meanwhile.
40154ab
to
ff0e080
Compare
|
||
it "sets the `:runner` option with the `bisect` invocation" do | ||
bisect = double(:bisect) | ||
allow(RSpec::Core::Invocations).to receive(:bisect).and_return(bisect) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stubbing invocations to return a double just so we can assert on equality below seems pretty sub-optimal. As I see it, the RSpec::Core::Invocations
methods are essentially just exposing constants -- given that they take no arguments. So WDYT about turning them into constants, and then you don't have to stub here -- instead, down below it can do:
expect(options[:runner]).to eq(RSpec::Core::Invocations::BISECT)
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside there is it doesn't let you capture extra info in the callable (as required by .print_help
). I do agree though, I don't particularly like the stubbing. Maybe we could turn them into callable objects?
module Invocations
PrintHelp = Struct.new(:parser, :invalid_options) do
def call(options, err, out)
#...
end
end
end
which would let us assert:
expect(options[:runner]).to be_instance_of(RSpec::Core::Invocations::PrintHelp)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The downside there is it doesn't let you capture extra info in the callable (as required by .print_help).
I missed that little detail. Thanks for pointing it out!
Maybe we could turn them into callable objects?
👍
I like this approach a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
4efce80
to
f0e5391
Compare
👍 LGTM. I plan to merge when the build goes green. |
Previous work introduced a set of invocation callables that could be set in the OptionParser to run instead of the default RSpec::Core::Runner. Testing them via the the OptionParser seemed out-of-place, however, and made the test stimulus needlessly complex. This change extracts the invocations to a set of callable objects defined in the RSpec::Core::Invocations namespace, allowing them to be tested separately from the OptionParser.
f0e5391
to
d52c969
Compare
Missed out the |
Thanks, @urbanautomaton! |
As always, thanks for the great work, @urbanautomaton :). |
Thanks, and a pleasure as always. 😄 |
…pec-opts Support bisect in spec opts
[ci skip]
I'm breaking out a new PR to fix #2231, based on discussion in #2259.
To summarise progress: the infinite looping problem is solved by modifying the environment given to the child processes, so that only the parent process starts a bisection.
However, the existing invocation of
Bisect::Runner
from withinOptionParser
is problematic, becauseOptionParser
is invoked twice: once for CLI args, and once for args set in the environment. We need to invoke the bisection only once all args have been parsed, so that the correct commands can be issued to the child processes.@myronmarston has suggested we refactor the
OptionParser
so it no longer invokes any actions directly, but instead returns a callable that the main runner can invoke. The current diff contains a sketch implementation of what I understand this to mean.@myronmarston - is this sort of thing what you envisaged? (Obviously there'd be further refactoring to do, I just wanted to check I've got the right general idea...)