rspec-core 3.1.7 → 3.2.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +1 -0
  5. data/Changelog.md +84 -0
  6. data/README.md +10 -1
  7. data/lib/rspec/core.rb +28 -8
  8. data/lib/rspec/core/backport_random.rb +12 -9
  9. data/lib/rspec/core/configuration.rb +350 -112
  10. data/lib/rspec/core/configuration_options.rb +14 -7
  11. data/lib/rspec/core/dsl.rb +7 -4
  12. data/lib/rspec/core/example.rb +86 -50
  13. data/lib/rspec/core/example_group.rb +247 -86
  14. data/lib/rspec/core/filter_manager.rb +38 -93
  15. data/lib/rspec/core/flat_map.rb +4 -4
  16. data/lib/rspec/core/formatters.rb +10 -6
  17. data/lib/rspec/core/formatters/base_formatter.rb +7 -4
  18. data/lib/rspec/core/formatters/base_text_formatter.rb +12 -12
  19. data/lib/rspec/core/formatters/console_codes.rb +8 -7
  20. data/lib/rspec/core/formatters/deprecation_formatter.rb +5 -3
  21. data/lib/rspec/core/formatters/documentation_formatter.rb +10 -4
  22. data/lib/rspec/core/formatters/helpers.rb +6 -4
  23. data/lib/rspec/core/formatters/html_formatter.rb +13 -8
  24. data/lib/rspec/core/formatters/html_printer.rb +26 -10
  25. data/lib/rspec/core/formatters/profile_formatter.rb +10 -7
  26. data/lib/rspec/core/formatters/protocol.rb +27 -18
  27. data/lib/rspec/core/formatters/snippet_extractor.rb +14 -7
  28. data/lib/rspec/core/hooks.rb +252 -211
  29. data/lib/rspec/core/memoized_helpers.rb +16 -16
  30. data/lib/rspec/core/metadata.rb +67 -28
  31. data/lib/rspec/core/metadata_filter.rb +151 -24
  32. data/lib/rspec/core/minitest_assertions_adapter.rb +5 -2
  33. data/lib/rspec/core/mocking_adapters/flexmock.rb +1 -1
  34. data/lib/rspec/core/mocking_adapters/mocha.rb +8 -8
  35. data/lib/rspec/core/notifications.rb +155 -94
  36. data/lib/rspec/core/option_parser.rb +16 -10
  37. data/lib/rspec/core/pending.rb +11 -9
  38. data/lib/rspec/core/project_initializer.rb +1 -1
  39. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +10 -8
  40. data/lib/rspec/core/rake_task.rb +37 -52
  41. data/lib/rspec/core/reporter.rb +30 -7
  42. data/lib/rspec/core/ruby_project.rb +12 -4
  43. data/lib/rspec/core/runner.rb +5 -8
  44. data/lib/rspec/core/sandbox.rb +37 -0
  45. data/lib/rspec/core/shared_example_group.rb +41 -15
  46. data/lib/rspec/core/test_unit_assertions_adapter.rb +3 -3
  47. data/lib/rspec/core/version.rb +1 -1
  48. data/lib/rspec/core/warnings.rb +2 -2
  49. data/lib/rspec/core/world.rb +12 -28
  50. metadata +44 -31
  51. metadata.gz.sig +0 -0
@@ -20,7 +20,7 @@ module RSpec
20
20
  #
21
21
  # @example
22
22
  #
23
- # # explicit declaration of subject
23
+ # # Explicit declaration of subject.
24
24
  # describe Person do
25
25
  # subject { Person.new(:birthdate => 19.years.ago) }
26
26
  # it "should be eligible to vote" do
@@ -29,7 +29,7 @@ module RSpec
29
29
  # end
30
30
  # end
31
31
  #
32
- # # implicit subject => { Person.new }
32
+ # # Implicit subject => { Person.new }.
33
33
  # describe Person do
34
34
  # it "should be eligible to vote" do
35
35
  # subject.should be_eligible_to_vote
@@ -37,17 +37,17 @@ module RSpec
37
37
  # end
38
38
  # end
39
39
  #
40
- # # one-liner syntax - expectation is set on the subject
40
+ # # One-liner syntax - expectation is set on the subject.
41
41
  # describe Person do
42
42
  # it { is_expected.to be_eligible_to_vote }
43
43
  # # or
44
44
  # it { should be_eligible_to_vote }
45
45
  # end
46
46
  #
47
- # @note Because `subject` is designed to create state that is reset between
48
- # each example, and `before(:context)` is designed to setup state that is
49
- # shared across _all_ examples in an example group, `subject` is _not_
50
- # intended to be used in a `before(:context)` hook.
47
+ # @note Because `subject` is designed to create state that is reset
48
+ # between each example, and `before(:context)` is designed to setup
49
+ # state that is shared across _all_ examples in an example group,
50
+ # `subject` is _not_ intended to be used in a `before(:context)` hook.
51
51
  #
52
52
  # @see #should
53
53
  # @see #should_not
@@ -211,8 +211,8 @@ EOS
211
211
  # though we have yet to see this in practice. You've been warned.
212
212
  #
213
213
  # @note Because `let` is designed to create state that is reset between
214
- # each example, and `before(:context)` is designed to setup state that is
215
- # shared across _all_ examples in an example group, `let` is _not_
214
+ # each example, and `before(:context)` is designed to setup state that
215
+ # is shared across _all_ examples in an example group, `let` is _not_
216
216
  # intended to be used in a `before(:context)` hook.
217
217
  #
218
218
  # @example
@@ -221,10 +221,10 @@ EOS
221
221
  # let(:thing) { Thing.new }
222
222
  #
223
223
  # it "does something" do
224
- # # first invocation, executes block, memoizes and returns result
224
+ # # First invocation, executes block, memoizes and returns result.
225
225
  # thing.do_something
226
226
  #
227
- # # second invocation, returns the memoized value
227
+ # # Second invocation, returns the memoized value.
228
228
  # thing.should be_something
229
229
  # end
230
230
  # end
@@ -302,8 +302,8 @@ EOS
302
302
  end
303
303
 
304
304
  # Declares a `subject` for an example group which can then be wrapped
305
- # with `expect` using `is_expected` to make it the target of an expectation
306
- # in a concise, one-line example.
305
+ # with `expect` using `is_expected` to make it the target of an
306
+ # expectation in a concise, one-line example.
307
307
  #
308
308
  # Given a `name`, defines a method with that name which returns the
309
309
  # `subject`. This lets you declare the subject once and access it
@@ -348,9 +348,9 @@ EOS
348
348
  end
349
349
  end
350
350
 
351
- # Just like `subject`, except the block is invoked by an implicit `before`
352
- # hook. This serves a dual purpose of setting up state and providing a
353
- # memoized reference to that state.
351
+ # Just like `subject`, except the block is invoked by an implicit
352
+ # `before` hook. This serves a dual purpose of setting up state and
353
+ # providing a memoized reference to that state.
354
354
  #
355
355
  # @example
356
356
  #
@@ -25,30 +25,51 @@ module RSpec
25
25
  # @see Configuration#filter_run_including
26
26
  # @see Configuration#filter_run_excluding
27
27
  module Metadata
28
+ # Matches strings either at the beginning of the input or prefixed with a
29
+ # whitespace, containing the current path, either postfixed with the
30
+ # separator, or at the end of the string. Match groups are the character
31
+ # before and the character after the string if any.
32
+ #
33
+ # http://rubular.com/r/fT0gmX6VJX
34
+ # http://rubular.com/r/duOrD4i3wb
35
+ # http://rubular.com/r/sbAMHFrOx1
36
+ def self.relative_path_regex
37
+ @relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/
38
+ end
39
+
28
40
  # @api private
29
41
  #
30
42
  # @param line [String] current code line
31
43
  # @return [String] relative path to line
32
44
  def self.relative_path(line)
33
- # Matches strings either at the beginning of the input or prefixed with a whitespace,
34
- # containing the current path, either postfixed with the separator, or at the end of the string.
35
- # Match groups are the character before and the character after the string if any.
36
- #
37
- # http://rubular.com/r/fT0gmX6VJX
38
- # http://rubular.com/r/duOrD4i3wb
39
- # http://rubular.com/r/sbAMHFrOx1
40
- #
41
-
42
- regex = /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/
43
-
44
- line = line.sub(regex, "\\1.\\2")
45
- line = line.sub(/\A([^:]+:\d+)$/, '\\1')
46
- return nil if line == '-e:1'
45
+ line = line.sub(relative_path_regex, "\\1.\\2".freeze)
46
+ line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze)
47
+ return nil if line == '-e:1'.freeze
47
48
  line
48
49
  rescue SecurityError
49
50
  nil
50
51
  end
51
52
 
53
+ # @private
54
+ # Iteratively walks up from the given metadata through all
55
+ # example group ancestors, yielding each metadata hash along the way.
56
+ def self.ascending(metadata)
57
+ yield metadata
58
+ return unless (group_metadata = metadata.fetch(:example_group) { metadata[:parent_example_group] })
59
+
60
+ loop do
61
+ yield group_metadata
62
+ break unless (group_metadata = group_metadata[:parent_example_group])
63
+ end
64
+ end
65
+
66
+ # @private
67
+ # Returns an enumerator that iteratively walks up the given metadata through all
68
+ # example group ancestors, yielding each metadata hash along the way.
69
+ def self.ascend(metadata)
70
+ enum_for(:ascending, metadata)
71
+ end
72
+
52
73
  # @private
53
74
  # Used internally to build a hash from an args array.
54
75
  # Symbols are converted into hash keys with a value of `true`.
@@ -67,6 +88,17 @@ module RSpec
67
88
  hash
68
89
  end
69
90
 
91
+ # @private
92
+ def self.deep_hash_dup(object)
93
+ return object.dup if Array === object
94
+ return object unless Hash === object
95
+
96
+ object.inject(object.dup) do |duplicate, (key, value)|
97
+ duplicate[key] = deep_hash_dup(value)
98
+ duplicate
99
+ end
100
+ end
101
+
70
102
  # @private
71
103
  def self.backtrace_from(block)
72
104
  return caller unless block.respond_to?(:source_location)
@@ -114,10 +146,11 @@ module RSpec
114
146
  file_path_and_line_number_from(caller)
115
147
  end
116
148
 
117
- file_path = Metadata.relative_path(file_path)
118
- metadata[:file_path] = file_path
119
- metadata[:line_number] = line_number.to_i
120
- metadata[:location] = "#{file_path}:#{line_number}"
149
+ relative_file_path = Metadata.relative_path(file_path)
150
+ metadata[:file_path] = relative_file_path
151
+ metadata[:line_number] = line_number.to_i
152
+ metadata[:location] = "#{relative_file_path}:#{line_number}"
153
+ metadata[:absolute_file_path] = File.expand_path(relative_file_path)
121
154
  end
122
155
 
123
156
  def file_path_and_line_number_from(backtrace)
@@ -128,16 +161,16 @@ module RSpec
128
161
 
129
162
  def description_separator(parent_part, child_part)
130
163
  if parent_part.is_a?(Module) && child_part =~ /^(#|::|\.)/
131
- ''
164
+ ''.freeze
132
165
  else
133
- ' '
166
+ ' '.freeze
134
167
  end
135
168
  end
136
169
 
137
170
  def build_description_from(parent_description=nil, my_description=nil)
138
171
  return parent_description.to_s unless my_description
139
172
  separator = description_separator(parent_description, my_description)
140
- parent_description.to_s + separator + my_description.to_s
173
+ (parent_description.to_s + separator) << my_description.to_s
141
174
  end
142
175
 
143
176
  def ensure_valid_user_keys
@@ -171,9 +204,11 @@ module RSpec
171
204
  group_metadata.update(example_metadata)
172
205
 
173
206
  example_metadata[:example_group] = group_metadata
207
+ example_metadata[:shared_group_inclusion_backtrace] = SharedExampleGroupInclusionStackFrame.current_backtrace
174
208
  example_metadata.delete(:parent_example_group)
175
209
 
176
- hash = new(example_metadata, user_metadata, [description].compact, block)
210
+ description_args = description.nil? ? [] : [description]
211
+ hash = new(example_metadata, user_metadata, description_args, block)
177
212
  hash.populate
178
213
  hash.metadata
179
214
  end
@@ -215,16 +250,18 @@ module RSpec
215
250
  Proc.new do |hash, key|
216
251
  case key
217
252
  when :example_group
218
- # We commonly get here when rspec-core is applying a previously configured
219
- # filter rule, such as when a gem configures:
253
+ # We commonly get here when rspec-core is applying a previously
254
+ # configured filter rule, such as when a gem configures:
220
255
  #
221
256
  # RSpec.configure do |c|
222
257
  # c.include MyGemHelpers, :example_group => { :file_path => /spec\/my_gem_specs/ }
223
258
  # end
224
259
  #
225
- # It's confusing for a user to get a deprecation at this point in the code, so instead
226
- # we issue a deprecation from the config APIs that take a metadata hash, and MetadataFilter
227
- # sets this thread local to silence the warning here since it would be so confusing.
260
+ # It's confusing for a user to get a deprecation at this point in
261
+ # the code, so instead we issue a deprecation from the config APIs
262
+ # that take a metadata hash, and MetadataFilter sets this thread
263
+ # local to silence the warning here since it would be so
264
+ # confusing.
228
265
  unless RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations]
229
266
  RSpec.deprecate("The `:example_group` key in an example group's metadata hash",
230
267
  :replacement => "the example group's hash directly for the " \
@@ -275,6 +312,7 @@ module RSpec
275
312
  :parent_example_group,
276
313
  :execution_result,
277
314
  :file_path,
315
+ :absolute_file_path,
278
316
  :full_description,
279
317
  :line_number,
280
318
  :location,
@@ -405,7 +443,8 @@ module RSpec
405
443
  # `metadata[:example_group][:described_class]` when you use
406
444
  # anonymous controller specs) such that changes are written
407
445
  # back to the top-level metadata hash.
408
- # * Exposes the parent group metadata as `[:example_group][:example_group]`.
446
+ # * Exposes the parent group metadata as
447
+ # `[:example_group][:example_group]`.
409
448
  class LegacyExampleGroupHash
410
449
  include HashImitatable
411
450
 
@@ -8,13 +8,8 @@ module RSpec
8
8
  module MetadataFilter
9
9
  class << self
10
10
  # @private
11
- def any_apply?(filters, metadata)
12
- filters.any? { |k, v| filter_applies?(k, v, metadata) }
13
- end
14
-
15
- # @private
16
- def all_apply?(filters, metadata)
17
- filters.all? { |k, v| filter_applies?(k, v, metadata) }
11
+ def apply?(predicate, filters, metadata)
12
+ filters.__send__(predicate) { |k, v| filter_applies?(k, v, metadata) }
18
13
  end
19
14
 
20
15
  # @private
@@ -48,9 +43,8 @@ module RSpec
48
43
  end
49
44
 
50
45
  def location_filter_applies?(locations, metadata)
51
- # it ignores location filters for other files
52
- line_number = example_group_declaration_line(locations, metadata)
53
- line_number ? line_number_filter_applies?(line_number, metadata) : true
46
+ line_numbers = example_group_declaration_lines(locations, metadata)
47
+ line_numbers.empty? || line_number_filter_applies?(line_numbers, metadata)
54
48
  end
55
49
 
56
50
  def line_number_filter_applies?(line_numbers, metadata)
@@ -59,14 +53,13 @@ module RSpec
59
53
  end
60
54
 
61
55
  def relevant_line_numbers(metadata)
62
- return [] unless metadata
63
- [metadata[:line_number]].compact + (relevant_line_numbers(parent_of metadata))
56
+ Metadata.ascend(metadata).map { |meta| meta[:line_number] }
64
57
  end
65
58
 
66
- def example_group_declaration_line(locations, metadata)
67
- parent = parent_of(metadata)
68
- return nil unless parent
69
- locations[File.expand_path(parent[:file_path])]
59
+ def example_group_declaration_lines(locations, metadata)
60
+ FlatMap.flat_map(Metadata.ascend(metadata)) do |meta|
61
+ locations[meta[:absolute_file_path]]
62
+ end.uniq
70
63
  end
71
64
 
72
65
  def filters_apply?(key, value, metadata)
@@ -75,14 +68,6 @@ module RSpec
75
68
  value.all? { |k, v| filter_applies?(k, v, subhash) }
76
69
  end
77
70
 
78
- def parent_of(metadata)
79
- if metadata.key?(:example_group)
80
- metadata[:example_group]
81
- else
82
- metadata[:parent_example_group]
83
- end
84
- end
85
-
86
71
  def silence_metadata_example_group_deprecations
87
72
  RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] = true
88
73
  yield
@@ -91,5 +76,147 @@ module RSpec
91
76
  end
92
77
  end
93
78
  end
79
+
80
+ # Tracks a collection of filterable items (e.g. modules, hooks, etc)
81
+ # and provides an optimized API to get the applicable items for the
82
+ # metadata of an example or example group.
83
+ #
84
+ # There are two implementations, optimized for different uses.
85
+ # @private
86
+ module FilterableItemRepository
87
+ # This implementation is simple, and is optimized for frequent
88
+ # updates but rare queries. `append` and `prepend` do no extra
89
+ # processing, and no internal memoization is done, since this
90
+ # is not optimized for queries.
91
+ #
92
+ # This is ideal for use by a example or example group, which may
93
+ # be updated multiple times with globally configured hooks, etc,
94
+ # but will not be queried frequently by other examples or examle
95
+ # groups.
96
+ # @private
97
+ class UpdateOptimized
98
+ attr_reader :items_and_filters
99
+
100
+ def initialize(applies_predicate)
101
+ @applies_predicate = applies_predicate
102
+ @items_and_filters = []
103
+ end
104
+
105
+ def append(item, metadata)
106
+ @items_and_filters << [item, metadata]
107
+ end
108
+
109
+ def prepend(item, metadata)
110
+ @items_and_filters.unshift [item, metadata]
111
+ end
112
+
113
+ def items_for(request_meta)
114
+ @items_and_filters.each_with_object([]) do |(item, item_meta), to_return|
115
+ to_return << item if item_meta.empty? ||
116
+ MetadataFilter.apply?(@applies_predicate, item_meta, request_meta)
117
+ end
118
+ end
119
+
120
+ unless [].respond_to?(:each_with_object) # For 1.8.7
121
+ undef items_for
122
+ def items_for(request_meta)
123
+ @items_and_filters.inject([]) do |to_return, (item, item_meta)|
124
+ to_return << item if item_meta.empty? ||
125
+ MetadataFilter.apply?(@applies_predicate, item_meta, request_meta)
126
+ to_return
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # This implementation is much more complex, and is optimized for
133
+ # rare (or hopefully no) updates once the queries start. Updates
134
+ # incur a cost as it has to clear the memoization and keep track
135
+ # of applicable keys. Queries will be O(N) the first time an item
136
+ # is provided with a given set of applicable metadata; subsequent
137
+ # queries with items with the same set of applicable metadata will
138
+ # be O(1) due to internal memoization.
139
+ #
140
+ # This is ideal for use by config, where filterable items (e.g. hooks)
141
+ # are typically added at the start of the process (e.g. in `spec_helper`)
142
+ # and then repeatedly queried as example groups and examples are defined.
143
+ # @private
144
+ class QueryOptimized < UpdateOptimized
145
+ alias find_items_for items_for
146
+ private :find_items_for
147
+
148
+ def initialize(applies_predicate)
149
+ super
150
+ @applicable_keys = Set.new
151
+ @proc_keys = Set.new
152
+ @memoized_lookups = Hash.new do |hash, applicable_metadata|
153
+ hash[applicable_metadata] = find_items_for(applicable_metadata)
154
+ end
155
+ end
156
+
157
+ def append(item, metadata)
158
+ super
159
+ handle_mutation(metadata)
160
+ end
161
+
162
+ def prepend(item, metadata)
163
+ super
164
+ handle_mutation(metadata)
165
+ end
166
+
167
+ def items_for(metadata)
168
+ # The filtering of `metadata` to `applicable_metadata` is the key thing
169
+ # that makes the memoization actually useful in practice, since each
170
+ # example and example group have different metadata (e.g. location and
171
+ # description). By filtering to the metadata keys our items care about,
172
+ # we can ignore extra metadata keys that differ for each example/group.
173
+ # For example, given `config.include DBHelpers, :db`, example groups
174
+ # can be split into these two sets: those that are tagged with `:db` and those
175
+ # that are not. For each set, this method for the first group in the set is
176
+ # still an `O(N)` calculation, but all subsequent groups in the set will be
177
+ # constant time lookups when they call this method.
178
+ applicable_metadata = applicable_metadata_from(metadata)
179
+
180
+ if applicable_metadata.any? { |k, _| @proc_keys.include?(k) }
181
+ # It's unsafe to memoize lookups involving procs (since they can
182
+ # be non-deterministic), so we skip the memoization in this case.
183
+ find_items_for(applicable_metadata)
184
+ else
185
+ @memoized_lookups[applicable_metadata]
186
+ end
187
+ end
188
+
189
+ private
190
+
191
+ def handle_mutation(metadata)
192
+ @applicable_keys.merge(metadata.keys)
193
+ @proc_keys.merge(proc_keys_from metadata)
194
+ @memoized_lookups.clear
195
+ end
196
+
197
+ def applicable_metadata_from(metadata)
198
+ @applicable_keys.inject({}) do |hash, key|
199
+ hash[key] = metadata[key] if metadata.key?(key)
200
+ hash
201
+ end
202
+ end
203
+
204
+ def proc_keys_from(metadata)
205
+ metadata.each_with_object([]) do |(key, value), to_return|
206
+ to_return << key if Proc === value
207
+ end
208
+ end
209
+
210
+ unless [].respond_to?(:each_with_object) # For 1.8.7
211
+ undef proc_keys_from
212
+ def proc_keys_from(metadata)
213
+ metadata.inject([]) do |to_return, (key, value)|
214
+ to_return << key if Proc === value
215
+ to_return
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
94
221
  end
95
222
  end