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.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.document +1 -1
  4. data/.yardopts +2 -1
  5. data/Changelog.md +888 -2
  6. data/{License.txt → LICENSE.md} +6 -5
  7. data/README.md +165 -24
  8. data/lib/rspec/autorun.rb +1 -0
  9. data/lib/rspec/core/backtrace_formatter.rb +19 -20
  10. data/lib/rspec/core/bisect/coordinator.rb +62 -0
  11. data/lib/rspec/core/bisect/example_minimizer.rb +173 -0
  12. data/lib/rspec/core/bisect/fork_runner.rb +138 -0
  13. data/lib/rspec/core/bisect/server.rb +61 -0
  14. data/lib/rspec/core/bisect/shell_command.rb +126 -0
  15. data/lib/rspec/core/bisect/shell_runner.rb +73 -0
  16. data/lib/rspec/core/bisect/utilities.rb +69 -0
  17. data/lib/rspec/core/configuration.rb +1287 -246
  18. data/lib/rspec/core/configuration_options.rb +95 -35
  19. data/lib/rspec/core/did_you_mean.rb +46 -0
  20. data/lib/rspec/core/drb.rb +21 -12
  21. data/lib/rspec/core/dsl.rb +10 -6
  22. data/lib/rspec/core/example.rb +305 -113
  23. data/lib/rspec/core/example_group.rb +431 -223
  24. data/lib/rspec/core/example_status_persister.rb +235 -0
  25. data/lib/rspec/core/filter_manager.rb +86 -115
  26. data/lib/rspec/core/flat_map.rb +6 -4
  27. data/lib/rspec/core/formatters/base_bisect_formatter.rb +45 -0
  28. data/lib/rspec/core/formatters/base_formatter.rb +14 -116
  29. data/lib/rspec/core/formatters/base_text_formatter.rb +18 -21
  30. data/lib/rspec/core/formatters/bisect_drb_formatter.rb +29 -0
  31. data/lib/rspec/core/formatters/bisect_progress_formatter.rb +157 -0
  32. data/lib/rspec/core/formatters/console_codes.rb +29 -18
  33. data/lib/rspec/core/formatters/deprecation_formatter.rb +16 -16
  34. data/lib/rspec/core/formatters/documentation_formatter.rb +49 -16
  35. data/lib/rspec/core/formatters/exception_presenter.rb +525 -0
  36. data/lib/rspec/core/formatters/failure_list_formatter.rb +23 -0
  37. data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
  38. data/lib/rspec/core/formatters/helpers.rb +45 -15
  39. data/lib/rspec/core/formatters/html_formatter.rb +33 -28
  40. data/lib/rspec/core/formatters/html_printer.rb +30 -20
  41. data/lib/rspec/core/formatters/html_snippet_extractor.rb +120 -0
  42. data/lib/rspec/core/formatters/json_formatter.rb +18 -9
  43. data/lib/rspec/core/formatters/profile_formatter.rb +10 -9
  44. data/lib/rspec/core/formatters/progress_formatter.rb +5 -4
  45. data/lib/rspec/core/formatters/protocol.rb +182 -0
  46. data/lib/rspec/core/formatters/snippet_extractor.rb +113 -82
  47. data/lib/rspec/core/formatters/syntax_highlighter.rb +91 -0
  48. data/lib/rspec/core/formatters.rb +81 -41
  49. data/lib/rspec/core/hooks.rb +314 -244
  50. data/lib/rspec/core/invocations.rb +87 -0
  51. data/lib/rspec/core/memoized_helpers.rb +161 -51
  52. data/lib/rspec/core/metadata.rb +132 -61
  53. data/lib/rspec/core/metadata_filter.rb +224 -64
  54. data/lib/rspec/core/minitest_assertions_adapter.rb +6 -3
  55. data/lib/rspec/core/mocking_adapters/flexmock.rb +4 -2
  56. data/lib/rspec/core/mocking_adapters/mocha.rb +11 -9
  57. data/lib/rspec/core/mocking_adapters/null.rb +2 -0
  58. data/lib/rspec/core/mocking_adapters/rr.rb +3 -1
  59. data/lib/rspec/core/mocking_adapters/rspec.rb +3 -1
  60. data/lib/rspec/core/notifications.rb +192 -206
  61. data/lib/rspec/core/option_parser.rb +174 -69
  62. data/lib/rspec/core/ordering.rb +48 -35
  63. data/lib/rspec/core/output_wrapper.rb +29 -0
  64. data/lib/rspec/core/pending.rb +25 -33
  65. data/lib/rspec/core/profiler.rb +34 -0
  66. data/lib/rspec/core/project_initializer/.rspec +0 -2
  67. data/lib/rspec/core/project_initializer/spec/spec_helper.rb +59 -39
  68. data/lib/rspec/core/project_initializer.rb +5 -3
  69. data/lib/rspec/core/rake_task.rb +99 -55
  70. data/lib/rspec/core/reporter.rb +128 -15
  71. data/lib/rspec/core/ruby_project.rb +14 -6
  72. data/lib/rspec/core/runner.rb +96 -45
  73. data/lib/rspec/core/sandbox.rb +37 -0
  74. data/lib/rspec/core/set.rb +54 -0
  75. data/lib/rspec/core/shared_example_group.rb +133 -43
  76. data/lib/rspec/core/shell_escape.rb +49 -0
  77. data/lib/rspec/core/test_unit_assertions_adapter.rb +4 -4
  78. data/lib/rspec/core/version.rb +1 -1
  79. data/lib/rspec/core/warnings.rb +6 -6
  80. data/lib/rspec/core/world.rb +172 -68
  81. data/lib/rspec/core.rb +66 -21
  82. data.tar.gz.sig +0 -0
  83. metadata +93 -69
  84. metadata.gz.sig +0 -0
  85. data/lib/rspec/core/backport_random.rb +0 -336
@@ -7,7 +7,7 @@ module RSpec
7
7
  # In addition to metadata that is used internally, this also stores
8
8
  # user-supplied metadata, e.g.
9
9
  #
10
- # describe Something, :type => :ui do
10
+ # RSpec.describe Something, :type => :ui do
11
11
  # it "does something", :slow => true do
12
12
  # # ...
13
13
  # end
@@ -25,17 +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
- line = line.sub(File.expand_path("."), ".")
34
- line = line.sub(/\A([^:]+:\d+)$/, '\\1')
35
- 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
36
48
  line
37
49
  rescue SecurityError
50
+ # :nocov:
38
51
  nil
52
+ # :nocov:
53
+ end
54
+
55
+ # @private
56
+ # Iteratively walks up from the given metadata through all
57
+ # example group ancestors, yielding each metadata hash along the way.
58
+ def self.ascending(metadata)
59
+ yield metadata
60
+ return unless (group_metadata = metadata.fetch(:example_group) { metadata[:parent_example_group] })
61
+
62
+ loop do
63
+ yield group_metadata
64
+ break unless (group_metadata = group_metadata[:parent_example_group])
65
+ end
66
+ end
67
+
68
+ # @private
69
+ # Returns an enumerator that iteratively walks up the given metadata through all
70
+ # example group ancestors, yielding each metadata hash along the way.
71
+ def self.ascend(metadata)
72
+ enum_for(:ascending, metadata)
39
73
  end
40
74
 
41
75
  # @private
@@ -46,9 +80,7 @@ module RSpec
46
80
  def self.build_hash_from(args, warn_about_example_group_filtering=false)
47
81
  hash = args.last.is_a?(Hash) ? args.pop : {}
48
82
 
49
- while args.last.is_a?(Symbol)
50
- hash[args.pop] = true
51
- end
83
+ hash[args.pop] = true while args.last.is_a?(Symbol)
52
84
 
53
85
  if warn_about_example_group_filtering && hash.key?(:example_group)
54
86
  RSpec.deprecate("Filtering by an `:example_group` subhash",
@@ -59,9 +91,24 @@ module RSpec
59
91
  end
60
92
 
61
93
  # @private
62
- def self.backtrace_from(block)
63
- return caller unless block.respond_to?(:source_location)
64
- [block.source_location.join(':')]
94
+ def self.deep_hash_dup(object)
95
+ return object.dup if Array === object
96
+ return object unless Hash === object
97
+
98
+ object.inject(object.dup) do |duplicate, (key, value)|
99
+ duplicate[key] = deep_hash_dup(value)
100
+ duplicate
101
+ end
102
+ end
103
+
104
+ # @private
105
+ def self.id_from(metadata)
106
+ "#{metadata[:rerun_file_path]}[#{metadata[:scoped_id]}]"
107
+ end
108
+
109
+ # @private
110
+ def self.location_tuple_from(metadata)
111
+ [metadata[:absolute_file_path], metadata[:line_number]]
65
112
  end
66
113
 
67
114
  # @private
@@ -70,9 +117,10 @@ module RSpec
70
117
  class HashPopulator
71
118
  attr_reader :metadata, :user_metadata, :description_args, :block
72
119
 
73
- def initialize(metadata, user_metadata, description_args, block)
120
+ def initialize(metadata, user_metadata, index_provider, description_args, block)
74
121
  @metadata = metadata
75
122
  @user_metadata = user_metadata
123
+ @index_provider = index_provider
76
124
  @description_args = description_args
77
125
  @block = block
78
126
  end
@@ -80,7 +128,6 @@ module RSpec
80
128
  def populate
81
129
  ensure_valid_user_keys
82
130
 
83
- metadata[:execution_result] = Example::ExecutionResult.new
84
131
  metadata[:block] = block
85
132
  metadata[:description_args] = description_args
86
133
  metadata[:description] = build_description_from(*metadata[:description_args])
@@ -89,81 +136,95 @@ module RSpec
89
136
 
90
137
  populate_location_attributes
91
138
  metadata.update(user_metadata)
92
- RSpec.configuration.apply_derived_metadata_to(metadata)
93
139
  end
94
140
 
95
141
  private
96
142
 
97
143
  def populate_location_attributes
98
- file_path, line_number = if backtrace = user_metadata.delete(:caller)
99
- file_path_and_line_number_from(backtrace)
100
- elsif block.respond_to?(:source_location)
101
- block.source_location
102
- else
103
- file_path_and_line_number_from(caller)
104
- end
105
-
106
- file_path = Metadata.relative_path(file_path)
107
- metadata[:file_path] = file_path
108
- metadata[:line_number] = line_number.to_i
109
- metadata[:location] = "#{file_path}:#{line_number}"
144
+ backtrace = user_metadata.delete(:caller)
145
+
146
+ file_path, line_number = if backtrace
147
+ file_path_and_line_number_from(backtrace)
148
+ elsif block.respond_to?(:source_location)
149
+ block.source_location
150
+ else
151
+ file_path_and_line_number_from(caller)
152
+ end
153
+
154
+ relative_file_path = Metadata.relative_path(file_path)
155
+ absolute_file_path = File.expand_path(relative_file_path)
156
+ metadata[:file_path] = relative_file_path
157
+ metadata[:line_number] = line_number.to_i
158
+ metadata[:location] = "#{relative_file_path}:#{line_number}"
159
+ metadata[:absolute_file_path] = absolute_file_path
160
+ metadata[:rerun_file_path] ||= relative_file_path
161
+ metadata[:scoped_id] = build_scoped_id_for(absolute_file_path)
110
162
  end
111
163
 
112
164
  def file_path_and_line_number_from(backtrace)
113
- first_caller_from_outside_rspec = backtrace.detect { |l| l !~ CallerFilter::LIB_REGEX }
165
+ first_caller_from_outside_rspec = backtrace.find { |l| l !~ CallerFilter::LIB_REGEX }
114
166
  first_caller_from_outside_rspec ||= backtrace.first
115
167
  /(.+?):(\d+)(?:|:\d+)/.match(first_caller_from_outside_rspec).captures
116
168
  end
117
169
 
118
170
  def description_separator(parent_part, child_part)
119
- if parent_part.is_a?(Module) && child_part =~ /^(#|::|\.)/
120
- ''
171
+ if parent_part.is_a?(Module) && /^(?:#|::|\.)/.match(child_part.to_s)
172
+ ''.freeze
121
173
  else
122
- ' '
174
+ ' '.freeze
123
175
  end
124
176
  end
125
177
 
126
178
  def build_description_from(parent_description=nil, my_description=nil)
127
179
  return parent_description.to_s unless my_description
180
+ return my_description.to_s if parent_description.to_s == ''
128
181
  separator = description_separator(parent_description, my_description)
129
- parent_description.to_s + separator + my_description.to_s
182
+ (parent_description.to_s + separator) << my_description.to_s
183
+ end
184
+
185
+ def build_scoped_id_for(file_path)
186
+ index = @index_provider.call(file_path).to_s
187
+ parent_scoped_id = metadata.fetch(:scoped_id) { return index }
188
+ "#{parent_scoped_id}:#{index}"
130
189
  end
131
190
 
132
191
  def ensure_valid_user_keys
133
192
  RESERVED_KEYS.each do |key|
134
- if user_metadata.has_key?(key)
135
- raise <<-EOM.gsub(/^\s+\|/, '')
136
- |#{"*"*50}
137
- |:#{key} is not allowed
138
- |
139
- |RSpec reserves some hash keys for its own internal use,
140
- |including :#{key}, which is used on:
141
- |
142
- | #{CallerFilter.first_non_rspec_line}.
143
- |
144
- |Here are all of RSpec's reserved hash keys:
145
- |
146
- | #{RESERVED_KEYS.join("\n ")}
147
- |#{"*"*50}
148
- EOM
149
- end
193
+ next unless user_metadata.key?(key)
194
+ raise <<-EOM.gsub(/^\s+\|/, '')
195
+ |#{"*" * 50}
196
+ |:#{key} is not allowed
197
+ |
198
+ |RSpec reserves some hash keys for its own internal use,
199
+ |including :#{key}, which is used on:
200
+ |
201
+ | #{CallerFilter.first_non_rspec_line}.
202
+ |
203
+ |Here are all of RSpec's reserved hash keys:
204
+ |
205
+ | #{RESERVED_KEYS.join("\n ")}
206
+ |#{"*" * 50}
207
+ EOM
150
208
  end
151
209
  end
152
210
  end
153
211
 
154
212
  # @private
155
213
  class ExampleHash < HashPopulator
156
- def self.create(group_metadata, user_metadata, description, block)
214
+ def self.create(group_metadata, user_metadata, index_provider, description, block)
157
215
  example_metadata = group_metadata.dup
158
216
  group_metadata = Hash.new(&ExampleGroupHash.backwards_compatibility_default_proc do |hash|
159
217
  hash[:parent_example_group]
160
218
  end)
161
219
  group_metadata.update(example_metadata)
162
220
 
221
+ example_metadata[:execution_result] = Example::ExecutionResult.new
163
222
  example_metadata[:example_group] = group_metadata
223
+ example_metadata[:shared_group_inclusion_backtrace] = SharedExampleGroupInclusionStackFrame.current_backtrace
164
224
  example_metadata.delete(:parent_example_group)
165
225
 
166
- hash = new(example_metadata, user_metadata, [description].compact, block)
226
+ description_args = description.nil? ? [] : [description]
227
+ hash = new(example_metadata, user_metadata, index_provider, description_args, block)
167
228
  hash.populate
168
229
  hash.metadata
169
230
  end
@@ -184,7 +245,7 @@ module RSpec
184
245
 
185
246
  # @private
186
247
  class ExampleGroupHash < HashPopulator
187
- def self.create(parent_group_metadata, user_metadata, *args, &block)
248
+ def self.create(parent_group_metadata, user_metadata, example_group_index, *args, &block)
188
249
  group_metadata = hash_with_backwards_compatibility_default_proc
189
250
 
190
251
  if parent_group_metadata
@@ -192,7 +253,7 @@ module RSpec
192
253
  group_metadata[:parent_example_group] = parent_group_metadata
193
254
  end
194
255
 
195
- hash = new(group_metadata, user_metadata, args, block)
256
+ hash = new(group_metadata, user_metadata, example_group_index, args, block)
196
257
  hash.populate
197
258
  hash.metadata
198
259
  end
@@ -205,20 +266,22 @@ module RSpec
205
266
  Proc.new do |hash, key|
206
267
  case key
207
268
  when :example_group
208
- # We commonly get here when rspec-core is applying a previously configured
209
- # filter rule, such as when a gem configures:
269
+ # We commonly get here when rspec-core is applying a previously
270
+ # configured filter rule, such as when a gem configures:
210
271
  #
211
272
  # RSpec.configure do |c|
212
273
  # c.include MyGemHelpers, :example_group => { :file_path => /spec\/my_gem_specs/ }
213
274
  # end
214
275
  #
215
- # It's confusing for a user to get a deprecation at this point in the code, so instead
216
- # we issue a deprecation from the config APIs that take a metadata hash, and MetadataFilter
217
- # sets this thread local to silence the warning here since it would be so confusing.
218
- unless RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations]
276
+ # It's confusing for a user to get a deprecation at this point in
277
+ # the code, so instead we issue a deprecation from the config APIs
278
+ # that take a metadata hash, and MetadataFilter sets this thread
279
+ # local to silence the warning here since it would be so
280
+ # confusing.
281
+ unless RSpec::Support.thread_local_data[:silence_metadata_example_group_deprecations]
219
282
  RSpec.deprecate("The `:example_group` key in an example group's metadata hash",
220
- :replacement => "the example group's hash directly for the " +
221
- "computed keys and `:parent_example_group` to access the parent " +
283
+ :replacement => "the example group's hash directly for the " \
284
+ "computed keys and `:parent_example_group` to access the parent " \
222
285
  "example group metadata")
223
286
  end
224
287
 
@@ -261,14 +324,21 @@ module RSpec
261
324
  # @private
262
325
  RESERVED_KEYS = [
263
326
  :description,
327
+ :description_args,
328
+ :described_class,
264
329
  :example_group,
265
330
  :parent_example_group,
266
331
  :execution_result,
332
+ :last_run_status,
267
333
  :file_path,
334
+ :absolute_file_path,
335
+ :rerun_file_path,
268
336
  :full_description,
269
337
  :line_number,
270
338
  :location,
271
- :block
339
+ :scoped_id,
340
+ :block,
341
+ :shared_group_inclusion_backtrace
272
342
  ]
273
343
  end
274
344
 
@@ -357,7 +427,7 @@ module RSpec
357
427
  to_h
358
428
  end
359
429
 
360
- def issue_deprecation(method_name, *args)
430
+ def issue_deprecation(_method_name, *_args)
361
431
  # no-op by default: subclasses can override
362
432
  end
363
433
 
@@ -395,7 +465,8 @@ module RSpec
395
465
  # `metadata[:example_group][:described_class]` when you use
396
466
  # anonymous controller specs) such that changes are written
397
467
  # back to the top-level metadata hash.
398
- # * Exposes the parent group metadata as `[:example_group][:example_group]`.
468
+ # * Exposes the parent group metadata as
469
+ # `[:example_group][:example_group]`.
399
470
  class LegacyExampleGroupHash
400
471
  include HashImitatable
401
472
 
@@ -6,89 +6,249 @@ module RSpec
6
6
  # having metadata be a raw hash (not a custom subclass), so externalizing
7
7
  # this filtering logic helps us move in that direction.
8
8
  module MetadataFilter
9
- extend self
9
+ class << self
10
+ # @private
11
+ def apply?(predicate, filters, metadata)
12
+ filters.__send__(predicate) { |k, v| filter_applies?(k, v, metadata) }
13
+ end
10
14
 
11
- # @private
12
- def any_apply?(filters, metadata)
13
- filters.any? { |k, v| filter_applies?(k, v, metadata) }
14
- end
15
+ # @private
16
+ def filter_applies?(key, filter_value, metadata)
17
+ silence_metadata_example_group_deprecations do
18
+ return location_filter_applies?(filter_value, metadata) if key == :locations
19
+ return id_filter_applies?(filter_value, metadata) if key == :ids
20
+ return filters_apply?(key, filter_value, metadata) if Hash === filter_value
15
21
 
16
- # @private
17
- def all_apply?(filters, metadata)
18
- filters.all? { |k, v| filter_applies?(k, v, metadata) }
22
+ meta_value = metadata.fetch(key) { return false }
23
+
24
+ return true if TrueClass === filter_value && meta_value
25
+ return proc_filter_applies?(key, filter_value, metadata) if Proc === filter_value
26
+ return filter_applies_to_any_value?(key, filter_value, metadata) if Array === meta_value
27
+
28
+ filter_value === meta_value || filter_value.to_s == meta_value.to_s
29
+ end
30
+ end
31
+
32
+ # @private
33
+ def silence_metadata_example_group_deprecations
34
+ RSpec::Support.thread_local_data[:silence_metadata_example_group_deprecations] = true
35
+ yield
36
+ ensure
37
+ RSpec::Support.thread_local_data.delete(:silence_metadata_example_group_deprecations)
38
+ end
39
+
40
+ private
41
+
42
+ def filter_applies_to_any_value?(key, value, metadata)
43
+ metadata[key].any? { |v| filter_applies?(key, v, key => value) }
44
+ end
45
+
46
+ def id_filter_applies?(rerun_paths_to_scoped_ids, metadata)
47
+ scoped_ids = rerun_paths_to_scoped_ids.fetch(metadata[:rerun_file_path]) { return false }
48
+
49
+ Metadata.ascend(metadata).any? do |meta|
50
+ scoped_ids.include?(meta[:scoped_id])
51
+ end
52
+ end
53
+
54
+ def location_filter_applies?(locations, metadata)
55
+ Metadata.ascend(metadata).any? do |meta|
56
+ file_path = meta[:absolute_file_path]
57
+ line_num = meta[:line_number]
58
+
59
+ locations[file_path].any? do |filter_line_num|
60
+ line_num == RSpec.world.preceding_declaration_line(file_path, filter_line_num)
61
+ end
62
+ end
63
+ end
64
+
65
+ def proc_filter_applies?(key, proc, metadata)
66
+ case proc.arity
67
+ when 0 then proc.call
68
+ when 2 then proc.call(metadata[key], metadata)
69
+ else proc.call(metadata[key])
70
+ end
71
+ end
72
+
73
+ def filters_apply?(key, value, metadata)
74
+ subhash = metadata[key]
75
+ return false unless Hash === subhash || HashImitatable === subhash
76
+ value.all? { |k, v| filter_applies?(k, v, subhash) }
77
+ end
19
78
  end
79
+ end
20
80
 
81
+ # Tracks a collection of filterable items (e.g. modules, hooks, etc)
82
+ # and provides an optimized API to get the applicable items for the
83
+ # metadata of an example or example group.
84
+ #
85
+ # There are two implementations, optimized for different uses.
86
+ # @private
87
+ module FilterableItemRepository
88
+ # This implementation is simple, and is optimized for frequent
89
+ # updates but rare queries. `append` and `prepend` do no extra
90
+ # processing, and no internal memoization is done, since this
91
+ # is not optimized for queries.
92
+ #
93
+ # This is ideal for use by a example or example group, which may
94
+ # be updated multiple times with globally configured hooks, etc,
95
+ # but will not be queried frequently by other examples or example
96
+ # groups.
21
97
  # @private
22
- def filter_applies?(key, value, metadata)
23
- silence_metadata_example_group_deprecations do
24
- return filter_applies_to_any_value?(key, value, metadata) if Array === metadata[key] && !(Proc === value)
25
- return location_filter_applies?(value, metadata) if key == :locations
26
- return filters_apply?(key, value, metadata) if Hash === value
27
-
28
- return false unless metadata.has_key?(key)
29
-
30
- case value
31
- when Regexp
32
- metadata[key] =~ value
33
- when Proc
34
- case value.arity
35
- when 0 then value.call
36
- when 2 then value.call(metadata[key], metadata)
37
- else value.call(metadata[key])
98
+ class UpdateOptimized
99
+ attr_reader :items_and_filters
100
+
101
+ def initialize(applies_predicate)
102
+ @applies_predicate = applies_predicate
103
+ @items_and_filters = []
104
+ end
105
+
106
+ def append(item, metadata)
107
+ @items_and_filters << [item, metadata]
108
+ end
109
+
110
+ def prepend(item, metadata)
111
+ @items_and_filters.unshift [item, metadata]
112
+ end
113
+
114
+ def delete(item, metadata)
115
+ @items_and_filters.delete [item, metadata]
116
+ end
117
+
118
+ def items_for(request_meta)
119
+ @items_and_filters.each_with_object([]) do |(item, item_meta), to_return|
120
+ to_return << item if item_meta.empty? ||
121
+ MetadataFilter.apply?(@applies_predicate, item_meta, request_meta)
122
+ end
123
+ end
124
+
125
+ unless [].respond_to?(:each_with_object) # For 1.8.7
126
+ # :nocov:
127
+ undef items_for
128
+ def items_for(request_meta)
129
+ @items_and_filters.inject([]) do |to_return, (item, item_meta)|
130
+ to_return << item if item_meta.empty? ||
131
+ MetadataFilter.apply?(@applies_predicate, item_meta, request_meta)
132
+ to_return
38
133
  end
39
- else
40
- metadata[key].to_s == value.to_s
41
134
  end
135
+ # :nocov:
42
136
  end
43
137
  end
44
138
 
45
- private
139
+ # This implementation is much more complex, and is optimized for
140
+ # rare (or hopefully no) updates once the queries start. Updates
141
+ # incur a cost as it has to clear the memoization and keep track
142
+ # of applicable keys. Queries will be O(N) the first time an item
143
+ # is provided with a given set of applicable metadata; subsequent
144
+ # queries with items with the same set of applicable metadata will
145
+ # be O(1) due to internal memoization.
146
+ #
147
+ # This is ideal for use by config, where filterable items (e.g. hooks)
148
+ # are typically added at the start of the process (e.g. in `spec_helper`)
149
+ # and then repeatedly queried as example groups and examples are defined.
150
+ # @private
151
+ class QueryOptimized < UpdateOptimized
152
+ alias find_items_for items_for
153
+ private :find_items_for
46
154
 
47
- def filter_applies_to_any_value?(key, value, metadata)
48
- metadata[key].any? {|v| filter_applies?(key, v, {key => value})}
49
- end
155
+ def initialize(applies_predicate)
156
+ super
157
+ @applicable_keys = Set.new
158
+ @proc_keys = Set.new
159
+ @memoized_lookups = Hash.new do |hash, applicable_metadata|
160
+ hash[applicable_metadata] = find_items_for(applicable_metadata)
161
+ end
162
+ end
50
163
 
51
- def location_filter_applies?(locations, metadata)
52
- # it ignores location filters for other files
53
- line_number = example_group_declaration_line(locations, metadata)
54
- line_number ? line_number_filter_applies?(line_number, metadata) : true
55
- end
164
+ def append(item, metadata)
165
+ super
166
+ handle_mutation(metadata)
167
+ end
56
168
 
57
- def line_number_filter_applies?(line_numbers, metadata)
58
- preceding_declaration_lines = line_numbers.map {|n| RSpec.world.preceding_declaration_line(n)}
59
- !(relevant_line_numbers(metadata) & preceding_declaration_lines).empty?
60
- end
169
+ def prepend(item, metadata)
170
+ super
171
+ handle_mutation(metadata)
172
+ end
61
173
 
62
- def relevant_line_numbers(metadata)
63
- return [] unless metadata
64
- [metadata[:line_number]].compact + (relevant_line_numbers(parent_of metadata))
65
- end
174
+ def delete(item, metadata)
175
+ super
176
+ reconstruct_caches
177
+ end
66
178
 
67
- def example_group_declaration_line(locations, metadata)
68
- parent = parent_of(metadata)
69
- return nil unless parent
70
- locations[File.expand_path(parent[:file_path])]
71
- end
179
+ def items_for(metadata)
180
+ # The filtering of `metadata` to `applicable_metadata` is the key thing
181
+ # that makes the memoization actually useful in practice, since each
182
+ # example and example group have different metadata (e.g. location and
183
+ # description). By filtering to the metadata keys our items care about,
184
+ # we can ignore extra metadata keys that differ for each example/group.
185
+ # For example, given `config.include DBHelpers, :db`, example groups
186
+ # can be split into these two sets: those that are tagged with `:db` and those
187
+ # that are not. For each set, this method for the first group in the set is
188
+ # still an `O(N)` calculation, but all subsequent groups in the set will be
189
+ # constant time lookups when they call this method.
190
+ applicable_metadata = applicable_metadata_from(metadata)
72
191
 
73
- def filters_apply?(key, value, metadata)
74
- subhash = metadata[key]
75
- return false unless Hash === subhash || HashImitatable === subhash
76
- value.all? { |k, v| filter_applies?(k, v, subhash) }
77
- end
192
+ if applicable_metadata.any? { |k, _| @proc_keys.include?(k) }
193
+ # It's unsafe to memoize lookups involving procs (since they can
194
+ # be non-deterministic), so we skip the memoization in this case.
195
+ find_items_for(applicable_metadata)
196
+ else
197
+ @memoized_lookups[applicable_metadata]
198
+ end
199
+ end
200
+
201
+ private
78
202
 
79
- def parent_of(metadata)
80
- if metadata.key?(:example_group)
81
- metadata[:example_group]
82
- else
83
- metadata[:parent_example_group]
203
+ def reconstruct_caches
204
+ @applicable_keys.clear
205
+ @proc_keys.clear
206
+ @items_and_filters.each do |_item, metadata|
207
+ handle_mutation(metadata)
208
+ end
84
209
  end
85
- end
86
210
 
87
- def silence_metadata_example_group_deprecations
88
- RSpec.thread_local_metadata[:silence_metadata_example_group_deprecations] = true
89
- yield
90
- ensure
91
- RSpec.thread_local_metadata.delete(:silence_metadata_example_group_deprecations)
211
+ def handle_mutation(metadata)
212
+ @applicable_keys.merge(metadata.keys)
213
+ @proc_keys.merge(proc_keys_from metadata)
214
+ @memoized_lookups.clear
215
+ end
216
+
217
+ def applicable_metadata_from(metadata)
218
+ MetadataFilter.silence_metadata_example_group_deprecations do
219
+ @applicable_keys.inject({}) do |hash, key|
220
+ # :example_group is treated special here because...
221
+ # - In RSpec 2, example groups had an `:example_group` key
222
+ # - In RSpec 3, that key is deprecated (it was confusing!).
223
+ # - The key is not technically present in an example group metadata hash
224
+ # (and thus would fail the `metadata.key?(key)` check) but a value
225
+ # is provided when accessed via the hash's `default_proc`
226
+ # - Thus, for backwards compatibility, we have to explicitly check
227
+ # for `:example_group` here if it is one of the keys being used to
228
+ # filter.
229
+ hash[key] = metadata[key] if metadata.key?(key) || key == :example_group
230
+ hash
231
+ end
232
+ end
233
+ end
234
+
235
+ def proc_keys_from(metadata)
236
+ metadata.each_with_object([]) do |(key, value), to_return|
237
+ to_return << key if Proc === value
238
+ end
239
+ end
240
+
241
+ unless [].respond_to?(:each_with_object) # For 1.8.7
242
+ # :nocov:
243
+ undef proc_keys_from
244
+ def proc_keys_from(metadata)
245
+ metadata.inject([]) do |to_return, (key, value)|
246
+ to_return << key if Proc === value
247
+ to_return
248
+ end
249
+ end
250
+ # :nocov:
251
+ end
92
252
  end
93
253
  end
94
254
  end