rspec-core 3.1.7 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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