rspec-core 3.0.4 → 3.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/.document +1 -1
- data/.yardopts +2 -1
- data/Changelog.md +888 -2
- data/{License.txt → LICENSE.md} +6 -5
- data/README.md +165 -24
- data/lib/rspec/autorun.rb +1 -0
- data/lib/rspec/core/backtrace_formatter.rb +19 -20
- data/lib/rspec/core/bisect/coordinator.rb +62 -0
- data/lib/rspec/core/bisect/example_minimizer.rb +173 -0
- data/lib/rspec/core/bisect/fork_runner.rb +138 -0
- data/lib/rspec/core/bisect/server.rb +61 -0
- data/lib/rspec/core/bisect/shell_command.rb +126 -0
- data/lib/rspec/core/bisect/shell_runner.rb +73 -0
- data/lib/rspec/core/bisect/utilities.rb +69 -0
- data/lib/rspec/core/configuration.rb +1287 -246
- data/lib/rspec/core/configuration_options.rb +95 -35
- data/lib/rspec/core/did_you_mean.rb +46 -0
- data/lib/rspec/core/drb.rb +21 -12
- data/lib/rspec/core/dsl.rb +10 -6
- data/lib/rspec/core/example.rb +305 -113
- data/lib/rspec/core/example_group.rb +431 -223
- data/lib/rspec/core/example_status_persister.rb +235 -0
- data/lib/rspec/core/filter_manager.rb +86 -115
- data/lib/rspec/core/flat_map.rb +6 -4
- data/lib/rspec/core/formatters/base_bisect_formatter.rb +45 -0
- data/lib/rspec/core/formatters/base_formatter.rb +14 -116
- data/lib/rspec/core/formatters/base_text_formatter.rb +18 -21
- data/lib/rspec/core/formatters/bisect_drb_formatter.rb +29 -0
- data/lib/rspec/core/formatters/bisect_progress_formatter.rb +157 -0
- data/lib/rspec/core/formatters/console_codes.rb +29 -18
- data/lib/rspec/core/formatters/deprecation_formatter.rb +16 -16
- data/lib/rspec/core/formatters/documentation_formatter.rb +49 -16
- data/lib/rspec/core/formatters/exception_presenter.rb +525 -0
- data/lib/rspec/core/formatters/failure_list_formatter.rb +23 -0
- data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
- data/lib/rspec/core/formatters/helpers.rb +45 -15
- data/lib/rspec/core/formatters/html_formatter.rb +33 -28
- data/lib/rspec/core/formatters/html_printer.rb +30 -20
- data/lib/rspec/core/formatters/html_snippet_extractor.rb +120 -0
- data/lib/rspec/core/formatters/json_formatter.rb +18 -9
- data/lib/rspec/core/formatters/profile_formatter.rb +10 -9
- data/lib/rspec/core/formatters/progress_formatter.rb +5 -4
- data/lib/rspec/core/formatters/protocol.rb +182 -0
- data/lib/rspec/core/formatters/snippet_extractor.rb +113 -82
- data/lib/rspec/core/formatters/syntax_highlighter.rb +91 -0
- data/lib/rspec/core/formatters.rb +81 -41
- data/lib/rspec/core/hooks.rb +314 -244
- data/lib/rspec/core/invocations.rb +87 -0
- data/lib/rspec/core/memoized_helpers.rb +161 -51
- data/lib/rspec/core/metadata.rb +132 -61
- data/lib/rspec/core/metadata_filter.rb +224 -64
- data/lib/rspec/core/minitest_assertions_adapter.rb +6 -3
- data/lib/rspec/core/mocking_adapters/flexmock.rb +4 -2
- data/lib/rspec/core/mocking_adapters/mocha.rb +11 -9
- data/lib/rspec/core/mocking_adapters/null.rb +2 -0
- data/lib/rspec/core/mocking_adapters/rr.rb +3 -1
- data/lib/rspec/core/mocking_adapters/rspec.rb +3 -1
- data/lib/rspec/core/notifications.rb +192 -206
- data/lib/rspec/core/option_parser.rb +174 -69
- data/lib/rspec/core/ordering.rb +48 -35
- data/lib/rspec/core/output_wrapper.rb +29 -0
- data/lib/rspec/core/pending.rb +25 -33
- data/lib/rspec/core/profiler.rb +34 -0
- data/lib/rspec/core/project_initializer/.rspec +0 -2
- data/lib/rspec/core/project_initializer/spec/spec_helper.rb +59 -39
- data/lib/rspec/core/project_initializer.rb +5 -3
- data/lib/rspec/core/rake_task.rb +99 -55
- data/lib/rspec/core/reporter.rb +128 -15
- data/lib/rspec/core/ruby_project.rb +14 -6
- data/lib/rspec/core/runner.rb +96 -45
- data/lib/rspec/core/sandbox.rb +37 -0
- data/lib/rspec/core/set.rb +54 -0
- data/lib/rspec/core/shared_example_group.rb +133 -43
- data/lib/rspec/core/shell_escape.rb +49 -0
- data/lib/rspec/core/test_unit_assertions_adapter.rb +4 -4
- data/lib/rspec/core/version.rb +1 -1
- data/lib/rspec/core/warnings.rb +6 -6
- data/lib/rspec/core/world.rb +172 -68
- data/lib/rspec/core.rb +66 -21
- data.tar.gz.sig +0 -0
- metadata +93 -69
- metadata.gz.sig +0 -0
- data/lib/rspec/core/backport_random.rb +0 -336
data/{License.txt → LICENSE.md}
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
=====================
|
|
2
3
|
|
|
3
|
-
Copyright
|
|
4
|
-
Copyright
|
|
5
|
-
Copyright
|
|
6
|
-
Copyright
|
|
4
|
+
* Copyright © 2012 Chad Humphries, David Chelimsky, Myron Marston
|
|
5
|
+
* Copyright © 2009 Chad Humphries, David Chelimsky
|
|
6
|
+
* Copyright © 2006 David Chelimsky, The RSpec Development Team
|
|
7
|
+
* Copyright © 2005 Steven Baker
|
|
7
8
|
|
|
8
9
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
9
10
|
a copy of this software and associated documentation files (the
|
data/README.md
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
# rspec-core [](https://github.com/rspec/rspec-core/actions) [](https://codeclimate.com/github/rspec/rspec-core)
|
|
2
2
|
|
|
3
3
|
rspec-core provides the structure for writing executable examples of how your
|
|
4
4
|
code should behave, and an `rspec` command with tools to constrain which
|
|
5
5
|
examples get run and tailor the output.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
gem install rspec # for rspec-core, rspec-expectations, rspec-mocks
|
|
10
10
|
gem install rspec-core # for rspec-core only
|
|
11
11
|
rspec --help
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Want to run against the `main` branch? You'll need to include the dependent
|
|
14
|
+
RSpec repos as well. Add the following to your `Gemfile`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
%w[rspec rspec-core rspec-expectations rspec-mocks rspec-support].each do |lib|
|
|
18
|
+
gem lib, :git => "https://github.com/rspec/#{lib}.git", :branch => 'main'
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Basic Structure
|
|
14
23
|
|
|
15
24
|
RSpec uses the words "describe" and "it" so we can express concepts like a conversation:
|
|
16
25
|
|
|
@@ -21,6 +30,7 @@ RSpec uses the words "describe" and "it" so we can express concepts like a conve
|
|
|
21
30
|
RSpec.describe Order do
|
|
22
31
|
it "sums the prices of its line items" do
|
|
23
32
|
order = Order.new
|
|
33
|
+
|
|
24
34
|
order.add_entry(LineItem.new(:item => Item.new(
|
|
25
35
|
:price => Money.new(1.11, :USD)
|
|
26
36
|
)))
|
|
@@ -28,6 +38,7 @@ RSpec.describe Order do
|
|
|
28
38
|
:price => Money.new(2.22, :USD),
|
|
29
39
|
:quantity => 2
|
|
30
40
|
)))
|
|
41
|
+
|
|
31
42
|
expect(order.total).to eq(Money.new(5.55, :USD))
|
|
32
43
|
end
|
|
33
44
|
end
|
|
@@ -40,9 +51,9 @@ Under the hood, an example group is a class in which the block passed to
|
|
|
40
51
|
`describe` is evaluated. The blocks passed to `it` are evaluated in the
|
|
41
52
|
context of an _instance_ of that class.
|
|
42
53
|
|
|
43
|
-
##
|
|
54
|
+
## Nested Groups
|
|
44
55
|
|
|
45
|
-
You can also declare nested
|
|
56
|
+
You can also declare nested groups using the `describe` or `context`
|
|
46
57
|
methods:
|
|
47
58
|
|
|
48
59
|
```ruby
|
|
@@ -61,7 +72,10 @@ RSpec.describe Order do
|
|
|
61
72
|
end
|
|
62
73
|
```
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
Nested groups are subclasses of the outer example group class, providing
|
|
76
|
+
the inheritance semantics you'd want for free.
|
|
77
|
+
|
|
78
|
+
## Aliases
|
|
65
79
|
|
|
66
80
|
You can declare example groups using either `describe` or `context`.
|
|
67
81
|
For a top level example group, `describe` and `context` are available
|
|
@@ -72,7 +86,7 @@ patching.
|
|
|
72
86
|
You can declare examples within a group using any of `it`, `specify`, or
|
|
73
87
|
`example`.
|
|
74
88
|
|
|
75
|
-
##
|
|
89
|
+
## Shared Examples and Contexts
|
|
76
90
|
|
|
77
91
|
Declare a shared example group using `shared_examples`, and then include it
|
|
78
92
|
in any group using `include_examples`.
|
|
@@ -102,7 +116,7 @@ pretty much the same as `shared_examples` and `include_examples`, providing
|
|
|
102
116
|
more accurate naming when you share hooks, `let` declarations, helper methods,
|
|
103
117
|
etc, but no examples.
|
|
104
118
|
|
|
105
|
-
##
|
|
119
|
+
## Metadata
|
|
106
120
|
|
|
107
121
|
rspec-core stores a metadata hash with every example and group, which
|
|
108
122
|
contains their descriptions, the locations at which they were
|
|
@@ -114,7 +128,7 @@ Although you probably won't ever need this unless you are writing an
|
|
|
114
128
|
extension, you can access it from an example like this:
|
|
115
129
|
|
|
116
130
|
```ruby
|
|
117
|
-
it "does something" do
|
|
131
|
+
it "does something" do |example|
|
|
118
132
|
expect(example.metadata[:description]).to eq("does something")
|
|
119
133
|
end
|
|
120
134
|
```
|
|
@@ -153,26 +167,106 @@ RSpec.describe Hash do
|
|
|
153
167
|
end
|
|
154
168
|
```
|
|
155
169
|
|
|
156
|
-
##
|
|
170
|
+
## A Word on Scope
|
|
171
|
+
|
|
172
|
+
RSpec has two scopes:
|
|
173
|
+
|
|
174
|
+
* **Example Group**: Example groups are defined by a `describe` or
|
|
175
|
+
`context` block, which is eagerly evaluated when the spec file is
|
|
176
|
+
loaded. The block is evaluated in the context of a subclass of
|
|
177
|
+
`RSpec::Core::ExampleGroup`, or a subclass of the parent example group
|
|
178
|
+
when you're nesting them.
|
|
179
|
+
* **Example**: Examples -- typically defined by an `it` block -- and any other
|
|
180
|
+
blocks with per-example semantics -- such as a `before(:example)` hook -- are
|
|
181
|
+
evaluated in the context of
|
|
182
|
+
an _instance_ of the example group class to which the example belongs.
|
|
183
|
+
Examples are _not_ executed when the spec file is loaded; instead,
|
|
184
|
+
RSpec waits to run any examples until all spec files have been loaded,
|
|
185
|
+
at which point it can apply filtering, randomization, etc.
|
|
186
|
+
|
|
187
|
+
To make this more concrete, consider this code snippet:
|
|
188
|
+
|
|
189
|
+
``` ruby
|
|
190
|
+
RSpec.describe "Using an array as a stack" do
|
|
191
|
+
def build_stack
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
before(:example) do
|
|
196
|
+
@stack = build_stack
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'is initially empty' do
|
|
200
|
+
expect(@stack).to be_empty
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
context "after an item has been pushed" do
|
|
204
|
+
before(:example) do
|
|
205
|
+
@stack.push :item
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'allows the pushed item to be popped' do
|
|
209
|
+
expect(@stack.pop).to eq(:item)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Under the covers, this is (roughly) equivalent to:
|
|
216
|
+
|
|
217
|
+
``` ruby
|
|
218
|
+
class UsingAnArrayAsAStack < RSpec::Core::ExampleGroup
|
|
219
|
+
def build_stack
|
|
220
|
+
[]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def before_example_1
|
|
224
|
+
@stack = build_stack
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def it_is_initially_empty
|
|
228
|
+
expect(@stack).to be_empty
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
class AfterAnItemHasBeenPushed < self
|
|
232
|
+
def before_example_2
|
|
233
|
+
@stack.push :item
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def it_allows_the_pushed_item_to_be_popped
|
|
237
|
+
expect(@stack.pop).to eq(:item)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
To run these examples, RSpec would (roughly) do the following:
|
|
244
|
+
|
|
245
|
+
``` ruby
|
|
246
|
+
example_1 = UsingAnArrayAsAStack.new
|
|
247
|
+
example_1.before_example_1
|
|
248
|
+
example_1.it_is_initially_empty
|
|
249
|
+
|
|
250
|
+
example_2 = UsingAnArrayAsAStack::AfterAnItemHasBeenPushed.new
|
|
251
|
+
example_2.before_example_1
|
|
252
|
+
example_2.before_example_2
|
|
253
|
+
example_2.it_allows_the_pushed_item_to_be_popped
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## The `rspec` Command
|
|
157
257
|
|
|
158
258
|
When you install the rspec-core gem, it installs the `rspec` executable,
|
|
159
259
|
which you'll use to run rspec. The `rspec` command comes with many useful
|
|
160
260
|
options.
|
|
161
261
|
Run `rspec --help` to see the complete list.
|
|
162
262
|
|
|
163
|
-
##
|
|
263
|
+
## Store Command Line Options `.rspec`
|
|
164
264
|
|
|
165
265
|
You can store command line options in a `.rspec` file in the project's root
|
|
166
266
|
directory, and the `rspec` command will read them as though you typed them on
|
|
167
267
|
the command line.
|
|
168
268
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
rspec-core no longer ships with an Autotest extension, if you require Autotest
|
|
172
|
-
integration, please use the `rspec-autotest` gem and see [rspec/rspec-autotest](https://github.com/rspec/rspec-autotest)
|
|
173
|
-
for details
|
|
174
|
-
|
|
175
|
-
## get started
|
|
269
|
+
## Get Started
|
|
176
270
|
|
|
177
271
|
Start with a simple example of behavior you expect from your system. Do
|
|
178
272
|
this before you write any implementation code:
|
|
@@ -195,13 +289,12 @@ $ rspec spec/calculator_spec.rb
|
|
|
195
289
|
./spec/calculator_spec.rb:1: uninitialized constant Calculator
|
|
196
290
|
```
|
|
197
291
|
|
|
198
|
-
|
|
292
|
+
Address the failure by defining a skeleton of the `Calculator` class:
|
|
199
293
|
|
|
200
294
|
```ruby
|
|
201
295
|
# in lib/calculator.rb
|
|
202
296
|
class Calculator
|
|
203
|
-
def add(a,b)
|
|
204
|
-
a + b
|
|
297
|
+
def add(a, b)
|
|
205
298
|
end
|
|
206
299
|
end
|
|
207
300
|
```
|
|
@@ -214,6 +307,39 @@ Be sure to require the implementation file in the spec:
|
|
|
214
307
|
require "calculator"
|
|
215
308
|
```
|
|
216
309
|
|
|
310
|
+
Now run the spec again, and watch the expectation fail:
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
$ rspec spec/calculator_spec.rb
|
|
314
|
+
F
|
|
315
|
+
|
|
316
|
+
Failures:
|
|
317
|
+
|
|
318
|
+
1) Calculator#add returns the sum of its arguments
|
|
319
|
+
Failure/Error: expect(Calculator.new.add(1, 2)).to eq(3)
|
|
320
|
+
|
|
321
|
+
expected: 3
|
|
322
|
+
got: nil
|
|
323
|
+
|
|
324
|
+
(compared using ==)
|
|
325
|
+
# ./spec/calculator_spec.rb:6:in `block (3 levels) in <top (required)>'
|
|
326
|
+
|
|
327
|
+
Finished in 0.00131 seconds (files took 0.10968 seconds to load)
|
|
328
|
+
1 example, 1 failure
|
|
329
|
+
|
|
330
|
+
Failed examples:
|
|
331
|
+
|
|
332
|
+
rspec ./spec/calculator_spec.rb:5 # Calculator#add returns the sum of its arguments
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Implement the simplest solution, by changing the definition of `Calculator#add` to:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
def add(a, b)
|
|
339
|
+
a + b
|
|
340
|
+
end
|
|
341
|
+
```
|
|
342
|
+
|
|
217
343
|
Now run the spec again, and watch it pass:
|
|
218
344
|
|
|
219
345
|
```
|
|
@@ -236,8 +362,23 @@ Finished in 0.000379 seconds
|
|
|
236
362
|
1 example, 0 failures
|
|
237
363
|
```
|
|
238
364
|
|
|
365
|
+
## Contributing
|
|
366
|
+
|
|
367
|
+
Once you've set up the environment, you'll need to cd into the working
|
|
368
|
+
directory of whichever repo you want to work in. From there you can run the
|
|
369
|
+
specs and cucumber features, and make patches.
|
|
370
|
+
|
|
371
|
+
NOTE: You do not need to use rspec-dev to work on a specific RSpec repo. You
|
|
372
|
+
can treat each RSpec repo as an independent project.
|
|
373
|
+
|
|
374
|
+
* [Build details](BUILD_DETAIL.md)
|
|
375
|
+
* [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
376
|
+
* [Detailed contributing guide](CONTRIBUTING.md)
|
|
377
|
+
* [Development setup guide](DEVELOPMENT.md)
|
|
378
|
+
|
|
239
379
|
## Also see
|
|
240
380
|
|
|
241
|
-
* [
|
|
242
|
-
* [
|
|
243
|
-
* [
|
|
381
|
+
* [https://github.com/rspec/rspec](https://github.com/rspec/rspec)
|
|
382
|
+
* [https://github.com/rspec/rspec-expectations](https://github.com/rspec/rspec-expectations)
|
|
383
|
+
* [https://github.com/rspec/rspec-mocks](https://github.com/rspec/rspec-mocks)
|
|
384
|
+
* [https://github.com/rspec/rspec-rails](https://github.com/rspec/rspec-rails)
|
data/lib/rspec/autorun.rb
CHANGED
|
@@ -8,28 +8,31 @@ module RSpec
|
|
|
8
8
|
def initialize
|
|
9
9
|
@full_backtrace = false
|
|
10
10
|
|
|
11
|
-
patterns = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"bin/",
|
|
15
|
-
"/gems/",
|
|
16
|
-
].map { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) }
|
|
11
|
+
patterns = %w[ /lib\d*/ruby/ bin/ exe/rspec /lib/bundler/ /exe/bundle: ]
|
|
12
|
+
patterns << "org/jruby/" if RUBY_PLATFORM == 'java'
|
|
13
|
+
patterns.map! { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) }
|
|
17
14
|
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@inclusion_patterns = [Regexp.new(Dir.getwd)]
|
|
21
|
-
end
|
|
15
|
+
@exclusion_patterns = [Regexp.union(RSpec::CallerFilter::IGNORE_REGEX, *patterns)]
|
|
16
|
+
@inclusion_patterns = []
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
return unless matches?(@exclusion_patterns, File.join(Dir.getwd, "lib", "foo.rb:13"))
|
|
19
|
+
inclusion_patterns << Regexp.new(Dir.getwd)
|
|
25
20
|
end
|
|
26
21
|
|
|
22
|
+
attr_writer :full_backtrace
|
|
23
|
+
|
|
27
24
|
def full_backtrace?
|
|
28
|
-
@full_backtrace ||
|
|
25
|
+
@full_backtrace || exclusion_patterns.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def filter_gem(gem_name)
|
|
29
|
+
sep = File::SEPARATOR
|
|
30
|
+
exclusion_patterns << /#{sep}#{gem_name}(-[^#{sep}]+)?#{sep}/
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def format_backtrace(backtrace, options
|
|
32
|
-
return
|
|
33
|
+
def format_backtrace(backtrace, options={})
|
|
34
|
+
return [] unless backtrace
|
|
35
|
+
return backtrace if options[:full_backtrace] || backtrace.empty?
|
|
33
36
|
|
|
34
37
|
backtrace.map { |l| backtrace_line(l) }.compact.
|
|
35
38
|
tap do |filtered|
|
|
@@ -45,15 +48,11 @@ module RSpec
|
|
|
45
48
|
|
|
46
49
|
def backtrace_line(line)
|
|
47
50
|
Metadata.relative_path(line) unless exclude?(line)
|
|
48
|
-
rescue SecurityError
|
|
49
|
-
nil
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def exclude?(line)
|
|
53
54
|
return false if @full_backtrace
|
|
54
|
-
|
|
55
|
-
return false unless matches?(@exclusion_patterns, relative_line)
|
|
56
|
-
matches?(@system_exclusion_patterns, relative_line) || !matches?(@inclusion_patterns, line)
|
|
55
|
+
matches?(exclusion_patterns, line) && !matches?(inclusion_patterns, line)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
private
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
RSpec::Support.require_rspec_core "bisect/shell_command"
|
|
2
|
+
RSpec::Support.require_rspec_core "bisect/example_minimizer"
|
|
3
|
+
RSpec::Support.require_rspec_core "bisect/utilities"
|
|
4
|
+
RSpec::Support.require_rspec_core "formatters/bisect_progress_formatter"
|
|
5
|
+
|
|
6
|
+
module RSpec
|
|
7
|
+
module Core
|
|
8
|
+
module Bisect
|
|
9
|
+
# The main entry point into the bisect logic. Coordinates among:
|
|
10
|
+
# - Bisect::ShellCommand: Generates shell commands to run spec subsets
|
|
11
|
+
# - Bisect::ExampleMinimizer: Contains the core bisect logic.
|
|
12
|
+
# - A bisect runner: runs a set of examples and returns the results.
|
|
13
|
+
# - A bisect formatter: provides progress updates to the user.
|
|
14
|
+
# @private
|
|
15
|
+
class Coordinator
|
|
16
|
+
def self.bisect_with(spec_runner, original_cli_args, formatter)
|
|
17
|
+
new(spec_runner, original_cli_args, formatter).bisect
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(spec_runner, original_cli_args, formatter)
|
|
21
|
+
@spec_runner = spec_runner
|
|
22
|
+
@shell_command = ShellCommand.new(original_cli_args)
|
|
23
|
+
@notifier = Bisect::Notifier.new(formatter)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bisect
|
|
27
|
+
repro = start_bisect_runner do |runner|
|
|
28
|
+
minimizer = ExampleMinimizer.new(@shell_command, runner, @notifier)
|
|
29
|
+
|
|
30
|
+
gracefully_abort_on_sigint(minimizer)
|
|
31
|
+
minimizer.find_minimal_repro
|
|
32
|
+
minimizer.repro_command_for_currently_needed_ids
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@notifier.publish(:bisect_repro_command, :repro => repro)
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
rescue BisectFailedError => e
|
|
39
|
+
@notifier.publish(:bisect_failed, :failure_explanation => e.message)
|
|
40
|
+
false
|
|
41
|
+
ensure
|
|
42
|
+
@notifier.publish(:close)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def start_bisect_runner(&block)
|
|
48
|
+
klass = @spec_runner.configuration.bisect_runner_class
|
|
49
|
+
klass.start(@shell_command, @spec_runner, &block)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def gracefully_abort_on_sigint(minimizer)
|
|
53
|
+
trap('INT') do
|
|
54
|
+
repro = minimizer.repro_command_for_currently_needed_ids
|
|
55
|
+
@notifier.publish(:bisect_aborted, :repro => repro)
|
|
56
|
+
exit(1)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
RSpec::Support.require_rspec_core "bisect/utilities"
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Core
|
|
5
|
+
module Bisect
|
|
6
|
+
# @private
|
|
7
|
+
# Contains the core bisect logic. Searches for examples we can ignore by
|
|
8
|
+
# repeatedly running different subsets of the suite.
|
|
9
|
+
class ExampleMinimizer
|
|
10
|
+
attr_reader :shell_command, :runner, :all_example_ids, :failed_example_ids
|
|
11
|
+
attr_accessor :remaining_ids
|
|
12
|
+
|
|
13
|
+
def initialize(shell_command, runner, notifier)
|
|
14
|
+
@shell_command = shell_command
|
|
15
|
+
@runner = runner
|
|
16
|
+
@notifier = notifier
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_minimal_repro
|
|
20
|
+
prep
|
|
21
|
+
|
|
22
|
+
_, duration = track_duration do
|
|
23
|
+
bisect(non_failing_example_ids)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
notify(:bisect_complete, :duration => duration,
|
|
27
|
+
:original_non_failing_count => non_failing_example_ids.size,
|
|
28
|
+
:remaining_count => remaining_ids.size)
|
|
29
|
+
|
|
30
|
+
remaining_ids + failed_example_ids
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def bisect(candidate_ids)
|
|
34
|
+
notify(:bisect_dependency_check_started)
|
|
35
|
+
if get_expected_failures_for?([])
|
|
36
|
+
notify(:bisect_dependency_check_failed)
|
|
37
|
+
self.remaining_ids = []
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
notify(:bisect_dependency_check_passed)
|
|
41
|
+
|
|
42
|
+
bisect_over(candidate_ids)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bisect_over(candidate_ids)
|
|
46
|
+
return if candidate_ids.one?
|
|
47
|
+
|
|
48
|
+
notify(
|
|
49
|
+
:bisect_round_started,
|
|
50
|
+
:candidate_range => example_range(candidate_ids),
|
|
51
|
+
:candidates_count => candidate_ids.size
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
slice_size = (candidate_ids.length / 2.0).ceil
|
|
55
|
+
lhs, rhs = candidate_ids.each_slice(slice_size).to_a
|
|
56
|
+
|
|
57
|
+
ids_to_ignore, duration = track_duration do
|
|
58
|
+
[lhs, rhs].find do |ids|
|
|
59
|
+
get_expected_failures_for?(remaining_ids - ids)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if ids_to_ignore
|
|
64
|
+
self.remaining_ids -= ids_to_ignore
|
|
65
|
+
notify(
|
|
66
|
+
:bisect_round_ignoring_ids,
|
|
67
|
+
:ids_to_ignore => ids_to_ignore,
|
|
68
|
+
:ignore_range => example_range(ids_to_ignore),
|
|
69
|
+
:remaining_ids => remaining_ids,
|
|
70
|
+
:duration => duration
|
|
71
|
+
)
|
|
72
|
+
bisect_over(candidate_ids - ids_to_ignore)
|
|
73
|
+
else
|
|
74
|
+
notify(
|
|
75
|
+
:bisect_round_detected_multiple_culprits,
|
|
76
|
+
:duration => duration
|
|
77
|
+
)
|
|
78
|
+
bisect_over(lhs)
|
|
79
|
+
bisect_over(rhs)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def currently_needed_ids
|
|
84
|
+
remaining_ids + failed_example_ids
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def repro_command_for_currently_needed_ids
|
|
88
|
+
return shell_command.repro_command_from(currently_needed_ids) if remaining_ids
|
|
89
|
+
"(Not yet enough information to provide any repro command)"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @private
|
|
93
|
+
# Convenience class for describing a subset of the candidate examples
|
|
94
|
+
ExampleRange = Struct.new(:start, :finish) do
|
|
95
|
+
def description
|
|
96
|
+
if start == finish
|
|
97
|
+
"example #{start}"
|
|
98
|
+
else
|
|
99
|
+
"examples #{start}-#{finish}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def example_range(ids)
|
|
107
|
+
ExampleRange.new(
|
|
108
|
+
non_failing_example_ids.find_index(ids.first) + 1,
|
|
109
|
+
non_failing_example_ids.find_index(ids.last) + 1
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def prep
|
|
114
|
+
notify(:bisect_starting, :original_cli_args => shell_command.original_cli_args,
|
|
115
|
+
:bisect_runner => runner.class.name)
|
|
116
|
+
|
|
117
|
+
_, duration = track_duration do
|
|
118
|
+
original_results = runner.original_results
|
|
119
|
+
@all_example_ids = original_results.all_example_ids
|
|
120
|
+
@failed_example_ids = original_results.failed_example_ids
|
|
121
|
+
@remaining_ids = non_failing_example_ids
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if @failed_example_ids.empty?
|
|
125
|
+
raise BisectFailedError, "\n\nNo failures found. Bisect only works " \
|
|
126
|
+
"in the presence of one or more failing examples."
|
|
127
|
+
else
|
|
128
|
+
notify(:bisect_original_run_complete, :failed_example_ids => failed_example_ids,
|
|
129
|
+
:non_failing_example_ids => non_failing_example_ids,
|
|
130
|
+
:duration => duration)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def non_failing_example_ids
|
|
135
|
+
@non_failing_example_ids ||= all_example_ids - failed_example_ids
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def get_expected_failures_for?(ids)
|
|
139
|
+
ids_to_run = ids + failed_example_ids
|
|
140
|
+
notify(
|
|
141
|
+
:bisect_individual_run_start,
|
|
142
|
+
:command => shell_command.repro_command_from(ids_to_run),
|
|
143
|
+
:ids_to_run => ids_to_run
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
results, duration = track_duration { runner.run(ids_to_run) }
|
|
147
|
+
notify(:bisect_individual_run_complete, :duration => duration, :results => results)
|
|
148
|
+
|
|
149
|
+
abort_if_ordering_inconsistent(results)
|
|
150
|
+
(failed_example_ids & results.failed_example_ids) == failed_example_ids
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def track_duration
|
|
154
|
+
start = ::RSpec::Core::Time.now
|
|
155
|
+
[yield, ::RSpec::Core::Time.now - start]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def abort_if_ordering_inconsistent(results)
|
|
159
|
+
expected_order = all_example_ids & results.all_example_ids
|
|
160
|
+
return if expected_order == results.all_example_ids
|
|
161
|
+
|
|
162
|
+
raise BisectFailedError, "\n\nThe example ordering is inconsistent. " \
|
|
163
|
+
"`--bisect` relies upon consistent ordering (e.g. by passing " \
|
|
164
|
+
"`--seed` if you're using random ordering) to work properly."
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def notify(*args)
|
|
168
|
+
@notifier.publish(*args)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|