rspec-core 3.2.3 → 3.3.0
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/Changelog.md +75 -0
- data/README.md +137 -20
- data/lib/rspec/autorun.rb +1 -0
- data/lib/rspec/core.rb +8 -16
- data/lib/rspec/core/backtrace_formatter.rb +1 -3
- data/lib/rspec/core/bisect/coordinator.rb +66 -0
- data/lib/rspec/core/bisect/example_minimizer.rb +130 -0
- data/lib/rspec/core/bisect/runner.rb +139 -0
- data/lib/rspec/core/bisect/server.rb +61 -0
- data/lib/rspec/core/bisect/subset_enumerator.rb +39 -0
- data/lib/rspec/core/configuration.rb +134 -5
- data/lib/rspec/core/configuration_options.rb +21 -10
- data/lib/rspec/core/example.rb +84 -50
- data/lib/rspec/core/example_group.rb +46 -18
- data/lib/rspec/core/example_status_persister.rb +235 -0
- data/lib/rspec/core/filter_manager.rb +43 -28
- data/lib/rspec/core/flat_map.rb +2 -0
- data/lib/rspec/core/formatters.rb +30 -20
- data/lib/rspec/core/formatters/base_text_formatter.rb +1 -0
- data/lib/rspec/core/formatters/bisect_formatter.rb +68 -0
- data/lib/rspec/core/formatters/bisect_progress_formatter.rb +115 -0
- data/lib/rspec/core/formatters/deprecation_formatter.rb +0 -1
- data/lib/rspec/core/formatters/documentation_formatter.rb +0 -4
- data/lib/rspec/core/formatters/exception_presenter.rb +389 -0
- data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
- data/lib/rspec/core/formatters/helpers.rb +22 -2
- data/lib/rspec/core/formatters/html_formatter.rb +1 -4
- data/lib/rspec/core/formatters/html_printer.rb +2 -6
- data/lib/rspec/core/formatters/json_formatter.rb +6 -4
- data/lib/rspec/core/formatters/snippet_extractor.rb +12 -7
- data/lib/rspec/core/hooks.rb +8 -2
- data/lib/rspec/core/memoized_helpers.rb +77 -17
- data/lib/rspec/core/metadata.rb +24 -10
- data/lib/rspec/core/metadata_filter.rb +16 -3
- data/lib/rspec/core/mutex.rb +63 -0
- data/lib/rspec/core/notifications.rb +84 -189
- data/lib/rspec/core/option_parser.rb +105 -32
- data/lib/rspec/core/ordering.rb +28 -25
- data/lib/rspec/core/profiler.rb +32 -0
- data/lib/rspec/core/project_initializer/spec/spec_helper.rb +6 -1
- data/lib/rspec/core/rake_task.rb +6 -20
- data/lib/rspec/core/reentrant_mutex.rb +52 -0
- data/lib/rspec/core/reporter.rb +65 -17
- data/lib/rspec/core/runner.rb +38 -14
- data/lib/rspec/core/set.rb +49 -0
- data/lib/rspec/core/shared_example_group.rb +3 -1
- data/lib/rspec/core/shell_escape.rb +49 -0
- data/lib/rspec/core/version.rb +1 -1
- data/lib/rspec/core/world.rb +31 -20
- metadata +35 -7
- metadata.gz.sig +0 -0
- data/lib/rspec/core/backport_random.rb +0 -339
@@ -0,0 +1,235 @@
|
|
1
|
+
RSpec::Support.require_rspec_support "directory_maker"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
# Persists example ids and their statuses so that we can filter
|
6
|
+
# to just the ones that failed the last time they ran.
|
7
|
+
# @private
|
8
|
+
class ExampleStatusPersister
|
9
|
+
def self.load_from(file_name)
|
10
|
+
return [] unless File.exist?(file_name)
|
11
|
+
ExampleStatusParser.parse(File.read(file_name))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.persist(examples, file_name)
|
15
|
+
new(examples, file_name).persist
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(examples, file_name)
|
19
|
+
@examples = examples
|
20
|
+
@file_name = file_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def persist
|
24
|
+
write dumped_statuses
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def write(statuses)
|
30
|
+
RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
|
31
|
+
File.open(@file_name, "w") { |f| f.write(statuses) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def dumped_statuses
|
35
|
+
ExampleStatusDumper.dump(merged_statuses)
|
36
|
+
end
|
37
|
+
|
38
|
+
def merged_statuses
|
39
|
+
ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs)
|
40
|
+
end
|
41
|
+
|
42
|
+
def statuses_from_this_run
|
43
|
+
@examples.map do |ex|
|
44
|
+
result = ex.execution_result
|
45
|
+
|
46
|
+
{
|
47
|
+
:example_id => ex.id,
|
48
|
+
:status => result.status ? result.status.to_s : Configuration::UNKNOWN_STATUS,
|
49
|
+
:run_time => result.run_time ? Formatters::Helpers.format_duration(result.run_time) : ""
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def statuses_from_previous_runs
|
55
|
+
self.class.load_from(@file_name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Merges together a list of example statuses from this run
|
60
|
+
# and a list from previous runs (presumably loaded from disk).
|
61
|
+
# Each example status object is expected to be a hash with
|
62
|
+
# at least an `:example_id` and a `:status` key. Examples that
|
63
|
+
# were loaded but not executed (due to filtering, `--fail-fast`
|
64
|
+
# or whatever) should have a `:status` of `UNKNOWN_STATUS`.
|
65
|
+
#
|
66
|
+
# This willl produce a new list that:
|
67
|
+
# - Will be missing examples from previous runs that we know for sure
|
68
|
+
# no longer exist.
|
69
|
+
# - Will have the latest known status for any examples that either
|
70
|
+
# definitively do exist or may still exist.
|
71
|
+
# - Is sorted by file name and example definition order, so that
|
72
|
+
# the saved file is easily scannable if users want to inspect it.
|
73
|
+
# @private
|
74
|
+
class ExampleStatusMerger
|
75
|
+
def self.merge(this_run, from_previous_runs)
|
76
|
+
new(this_run, from_previous_runs).merge
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(this_run, from_previous_runs)
|
80
|
+
@this_run = hash_from(this_run)
|
81
|
+
@from_previous_runs = hash_from(from_previous_runs)
|
82
|
+
@file_exists_cache = Hash.new { |hash, file| hash[file] = File.exist?(file) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def merge
|
86
|
+
delete_previous_examples_that_no_longer_exist
|
87
|
+
|
88
|
+
@this_run.merge(@from_previous_runs) do |_ex_id, new, old|
|
89
|
+
new.fetch(:status) == Configuration::UNKNOWN_STATUS ? old : new
|
90
|
+
end.values.sort_by(&method(:sort_value_from))
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def hash_from(example_list)
|
96
|
+
example_list.inject({}) do |hash, example|
|
97
|
+
hash[example.fetch(:example_id)] = example
|
98
|
+
hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def delete_previous_examples_that_no_longer_exist
|
103
|
+
@from_previous_runs.delete_if do |ex_id, _|
|
104
|
+
example_must_no_longer_exist?(ex_id)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def example_must_no_longer_exist?(ex_id)
|
109
|
+
# Obviously, it exists if it was loaded for this spec run...
|
110
|
+
return false if @this_run.key?(ex_id)
|
111
|
+
|
112
|
+
spec_file = spec_file_from(ex_id)
|
113
|
+
|
114
|
+
# `this_run` includes examples that were loaded but not executed.
|
115
|
+
# Given that, if the spec file for this example was loaded,
|
116
|
+
# but the id does not still exist, it's safe to assume that
|
117
|
+
# the example must no longer exist.
|
118
|
+
return true if loaded_spec_files.include?(spec_file)
|
119
|
+
|
120
|
+
# The example may still exist as long as the file exists...
|
121
|
+
!@file_exists_cache[spec_file]
|
122
|
+
end
|
123
|
+
|
124
|
+
def loaded_spec_files
|
125
|
+
@loaded_spec_files ||= Set.new(@this_run.keys.map(&method(:spec_file_from)))
|
126
|
+
end
|
127
|
+
|
128
|
+
def spec_file_from(ex_id)
|
129
|
+
ex_id.split("[").first
|
130
|
+
end
|
131
|
+
|
132
|
+
def sort_value_from(example)
|
133
|
+
file, scoped_id = example.fetch(:example_id).split(Configuration::ON_SQUARE_BRACKETS)
|
134
|
+
[file, *scoped_id.split(":").map(&method(:Integer))]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Dumps a list of hashes in a pretty, human readable format
|
139
|
+
# for later parsing. The hashes are expected to have symbol
|
140
|
+
# keys and string values, and each hash should have the same
|
141
|
+
# set of keys.
|
142
|
+
# @private
|
143
|
+
class ExampleStatusDumper
|
144
|
+
def self.dump(examples)
|
145
|
+
new(examples).dump
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize(examples)
|
149
|
+
@examples = examples
|
150
|
+
end
|
151
|
+
|
152
|
+
def dump
|
153
|
+
return nil if @examples.empty?
|
154
|
+
(formatted_header_rows + formatted_value_rows).join("\n") << "\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def formatted_header_rows
|
160
|
+
@formatted_header_rows ||= begin
|
161
|
+
dividers = column_widths.map { |w| "-" * w }
|
162
|
+
[formatted_row_from(headers.map(&:to_s)), formatted_row_from(dividers)]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def formatted_value_rows
|
167
|
+
@foramtted_value_rows ||= rows.map do |row|
|
168
|
+
formatted_row_from(row)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def rows
|
173
|
+
@rows ||= @examples.map { |ex| ex.values_at(*headers) }
|
174
|
+
end
|
175
|
+
|
176
|
+
def formatted_row_from(row_values)
|
177
|
+
padded_values = row_values.each_with_index.map do |value, index|
|
178
|
+
value.ljust(column_widths[index])
|
179
|
+
end
|
180
|
+
|
181
|
+
padded_values.join(" | ") << " |"
|
182
|
+
end
|
183
|
+
|
184
|
+
def headers
|
185
|
+
@headers ||= @examples.first.keys
|
186
|
+
end
|
187
|
+
|
188
|
+
def column_widths
|
189
|
+
@column_widths ||= begin
|
190
|
+
value_sets = rows.transpose
|
191
|
+
|
192
|
+
headers.each_with_index.map do |header, index|
|
193
|
+
values = value_sets[index] << header.to_s
|
194
|
+
values.map(&:length).max
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Parses a string that has been previously dumped by ExampleStatusDumper.
|
201
|
+
# Note that this parser is a bit naive in that it does a simple split on
|
202
|
+
# "\n" and " | ", with no concern for handling escaping. For now, that's
|
203
|
+
# OK because the values we plan to persist (example id, status, and perhaps
|
204
|
+
# example duration) are highly unlikely to contain "\n" or " | " -- after
|
205
|
+
# all, who puts those in file names?
|
206
|
+
# @private
|
207
|
+
class ExampleStatusParser
|
208
|
+
def self.parse(string)
|
209
|
+
new(string).parse
|
210
|
+
end
|
211
|
+
|
212
|
+
def initialize(string)
|
213
|
+
@header_line, _, *@row_lines = string.lines.to_a
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse
|
217
|
+
@row_lines.map { |line| parse_row(line) }
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def parse_row(line)
|
223
|
+
Hash[headers.zip(split_line(line))]
|
224
|
+
end
|
225
|
+
|
226
|
+
def headers
|
227
|
+
@headers ||= split_line(@header_line).grep(/\S/).map(&:to_sym)
|
228
|
+
end
|
229
|
+
|
230
|
+
def split_line(line)
|
231
|
+
line.split(/\s+\|\s+?/, -1)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -16,9 +16,15 @@ module RSpec
|
|
16
16
|
# locations is a hash of expanded paths to arrays of line
|
17
17
|
# numbers to match against. e.g.
|
18
18
|
# { "path/to/file.rb" => [37, 42] }
|
19
|
-
locations
|
20
|
-
|
21
|
-
|
19
|
+
add_path_to_arrays_filter(:locations, File.expand_path(file_path), line_numbers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_ids(rerun_path, scoped_ids)
|
23
|
+
# ids is a hash of relative paths to arrays of ids
|
24
|
+
# to match against. e.g.
|
25
|
+
# { "./path/to/file.rb" => ["1:1", "2:4"] }
|
26
|
+
rerun_path = Metadata.relative_path(File.expand_path rerun_path)
|
27
|
+
add_path_to_arrays_filter(:ids, rerun_path, scoped_ids)
|
22
28
|
end
|
23
29
|
|
24
30
|
def empty?
|
@@ -26,16 +32,23 @@ module RSpec
|
|
26
32
|
end
|
27
33
|
|
28
34
|
def prune(examples)
|
35
|
+
# Semantically, this is unnecessary (the filtering below will return the empty
|
36
|
+
# array unmodified), but for perf reasons it's worth exiting early here. Users
|
37
|
+
# commonly have top-level examples groups that do not have any direct examples
|
38
|
+
# and instead have nested groups with examples. In that kind of situation,
|
39
|
+
# `examples` will be empty.
|
40
|
+
return examples if examples.empty?
|
41
|
+
|
29
42
|
examples = prune_conditionally_filtered_examples(examples)
|
30
43
|
|
31
44
|
if inclusions.standalone?
|
32
|
-
examples.select { |e|
|
45
|
+
examples.select { |e| inclusions.include_example?(e) }
|
33
46
|
else
|
34
|
-
locations,
|
47
|
+
locations, ids, non_scoped_inclusions = inclusions.split_file_scoped_rules
|
35
48
|
|
36
|
-
examples.select do |
|
37
|
-
|
38
|
-
!
|
49
|
+
examples.select do |ex|
|
50
|
+
file_scoped_include?(ex.metadata, ids, locations) do
|
51
|
+
!exclusions.include_example?(ex) && non_scoped_inclusions.include_example?(ex)
|
39
52
|
end
|
40
53
|
end
|
41
54
|
end
|
@@ -67,12 +80,10 @@ module RSpec
|
|
67
80
|
|
68
81
|
private
|
69
82
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def include?(example)
|
75
|
-
inclusions.include_example?(example)
|
83
|
+
def add_path_to_arrays_filter(filter_key, path, values)
|
84
|
+
filter = inclusions.delete(filter_key) || Hash.new { |h, k| h[k] = [] }
|
85
|
+
filter[path].concat(values)
|
86
|
+
inclusions.add(filter_key => filter)
|
76
87
|
end
|
77
88
|
|
78
89
|
def prune_conditionally_filtered_examples(examples)
|
@@ -87,9 +98,16 @@ module RSpec
|
|
87
98
|
# and there is a `:slow => true` exclusion filter), but only for specs
|
88
99
|
# defined in the same file as the location filters. Excluded specs in
|
89
100
|
# other files should still be excluded.
|
90
|
-
def
|
91
|
-
|
92
|
-
|
101
|
+
def file_scoped_include?(ex_metadata, ids, locations)
|
102
|
+
no_id_filters = ids[ex_metadata[:rerun_file_path]].empty?
|
103
|
+
no_location_filters = locations[
|
104
|
+
File.expand_path(ex_metadata[:rerun_file_path])
|
105
|
+
].empty?
|
106
|
+
|
107
|
+
return yield if no_location_filters && no_id_filters
|
108
|
+
|
109
|
+
MetadataFilter.filter_applies?(:ids, ids, ex_metadata) ||
|
110
|
+
MetadataFilter.filter_applies?(:locations, locations, ex_metadata)
|
93
111
|
end
|
94
112
|
end
|
95
113
|
|
@@ -174,17 +192,6 @@ module RSpec
|
|
174
192
|
apply_standalone_filter(*args) || super
|
175
193
|
end
|
176
194
|
|
177
|
-
def use(*args)
|
178
|
-
apply_standalone_filter(*args) || super
|
179
|
-
end
|
180
|
-
|
181
|
-
def partition_locations
|
182
|
-
locations = @rules.fetch(:locations) { Hash.new([]) }
|
183
|
-
other_inclusions = self.class.new(@rules.dup.tap { |r| r.delete(:locations) })
|
184
|
-
|
185
|
-
return locations, other_inclusions
|
186
|
-
end
|
187
|
-
|
188
195
|
def include_example?(example)
|
189
196
|
@rules.empty? || super
|
190
197
|
end
|
@@ -193,6 +200,14 @@ module RSpec
|
|
193
200
|
is_standalone_filter?(@rules)
|
194
201
|
end
|
195
202
|
|
203
|
+
def split_file_scoped_rules
|
204
|
+
rules_dup = @rules.dup
|
205
|
+
locations = rules_dup.delete(:locations) { Hash.new([]) }
|
206
|
+
ids = rules_dup.delete(:ids) { Hash.new([]) }
|
207
|
+
|
208
|
+
return locations, ids, self.class.new(rules_dup)
|
209
|
+
end
|
210
|
+
|
196
211
|
private
|
197
212
|
|
198
213
|
def apply_standalone_filter(updated)
|
data/lib/rspec/core/flat_map.rb
CHANGED
@@ -66,11 +66,13 @@ RSpec::Support.require_rspec_support "directory_maker"
|
|
66
66
|
# @see RSpec::Core::Formatters::BaseTextFormatter
|
67
67
|
# @see RSpec::Core::Reporter
|
68
68
|
module RSpec::Core::Formatters
|
69
|
-
autoload :DocumentationFormatter,
|
70
|
-
autoload :HtmlFormatter,
|
71
|
-
autoload :
|
72
|
-
autoload :
|
73
|
-
autoload :
|
69
|
+
autoload :DocumentationFormatter, 'rspec/core/formatters/documentation_formatter'
|
70
|
+
autoload :HtmlFormatter, 'rspec/core/formatters/html_formatter'
|
71
|
+
autoload :FallbackMessageFormatter, 'rspec/core/formatters/fallback_message_formatter'
|
72
|
+
autoload :ProgressFormatter, 'rspec/core/formatters/progress_formatter'
|
73
|
+
autoload :ProfileFormatter, 'rspec/core/formatters/profile_formatter'
|
74
|
+
autoload :JsonFormatter, 'rspec/core/formatters/json_formatter'
|
75
|
+
autoload :BisectFormatter, 'rspec/core/formatters/bisect_formatter'
|
74
76
|
|
75
77
|
# Register the formatter class
|
76
78
|
# @param formatter_class [Class] formatter class to register
|
@@ -120,7 +122,15 @@ module RSpec::Core::Formatters
|
|
120
122
|
add DeprecationFormatter, deprecation_stream, output_stream
|
121
123
|
end
|
122
124
|
|
123
|
-
|
125
|
+
unless existing_formatter_implements?(:message)
|
126
|
+
add FallbackMessageFormatter, output_stream
|
127
|
+
end
|
128
|
+
|
129
|
+
return unless RSpec.configuration.profile_examples?
|
130
|
+
|
131
|
+
@reporter.setup_profiler
|
132
|
+
|
133
|
+
return if existing_formatter_implements?(:dump_profile)
|
124
134
|
|
125
135
|
add RSpec::Core::Formatters::ProfileFormatter, output_stream
|
126
136
|
end
|
@@ -133,18 +143,12 @@ module RSpec::Core::Formatters
|
|
133
143
|
|
134
144
|
if !Loader.formatters[formatter_class].nil?
|
135
145
|
formatter = formatter_class.new(*args)
|
136
|
-
|
146
|
+
register formatter, notifications_for(formatter_class)
|
137
147
|
elsif defined?(RSpec::LegacyFormatters)
|
138
148
|
formatter = RSpec::LegacyFormatters.load_formatter formatter_class, *args
|
139
|
-
|
149
|
+
register formatter, formatter.notifications
|
140
150
|
else
|
141
|
-
|
142
|
-
if line
|
143
|
-
call_site = "Formatter added at: #{line}"
|
144
|
-
else
|
145
|
-
call_site = "The formatter was added via command line flag or your "\
|
146
|
-
"`.rspec` file."
|
147
|
-
end
|
151
|
+
call_site = "Formatter added at: #{::RSpec::CallerFilter.first_non_rspec_line}"
|
148
152
|
|
149
153
|
RSpec.warn_deprecation <<-WARNING.gsub(/\s*\|/, ' ')
|
150
154
|
|The #{formatter_class} formatter uses the deprecated formatter
|
@@ -157,10 +161,7 @@ module RSpec::Core::Formatters
|
|
157
161
|
|
|
158
162
|
|#{call_site}
|
159
163
|
WARNING
|
160
|
-
return
|
161
164
|
end
|
162
|
-
@formatters << formatter unless duplicate_formatter_exists?(formatter)
|
163
|
-
formatter
|
164
165
|
end
|
165
166
|
|
166
167
|
private
|
@@ -172,6 +173,13 @@ module RSpec::Core::Formatters
|
|
172
173
|
"maybe you meant 'documentation' or 'progress'?.")
|
173
174
|
end
|
174
175
|
|
176
|
+
def register(formatter, notifications)
|
177
|
+
return if duplicate_formatter_exists?(formatter)
|
178
|
+
@reporter.register_listener formatter, *notifications
|
179
|
+
@formatters << formatter
|
180
|
+
formatter
|
181
|
+
end
|
182
|
+
|
175
183
|
def duplicate_formatter_exists?(new_formatter)
|
176
184
|
@formatters.any? do |formatter|
|
177
185
|
formatter.class === new_formatter && formatter.output == new_formatter.output
|
@@ -192,12 +200,14 @@ module RSpec::Core::Formatters
|
|
192
200
|
ProgressFormatter
|
193
201
|
when 'j', 'json'
|
194
202
|
JsonFormatter
|
203
|
+
when 'bisect'
|
204
|
+
BisectFormatter
|
195
205
|
end
|
196
206
|
end
|
197
207
|
|
198
208
|
def notifications_for(formatter_class)
|
199
|
-
formatter_class.ancestors.inject(Set.new) do |notifications, klass|
|
200
|
-
notifications
|
209
|
+
formatter_class.ancestors.inject(::RSpec::Core::Set.new) do |notifications, klass|
|
210
|
+
notifications.merge Loader.formatters.fetch(klass) { ::RSpec::Core::Set.new }
|
201
211
|
end
|
202
212
|
end
|
203
213
|
|