/
shared_example_group.rb
271 lines (238 loc) · 10 KB
/
shared_example_group.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
RSpec::Support.require_rspec_support "with_keywords_when_needed"
module RSpec
module Core
# Represents some functionality that is shared with multiple example groups.
# The functionality is defined by the provided block, which is lazily
# eval'd when the `SharedExampleGroupModule` instance is included in an example
# group.
class SharedExampleGroupModule < Module
# @private
attr_reader :definition
def initialize(description, definition, metadata)
@description = description
@definition = definition
@metadata = metadata
end
# Provides a human-readable representation of this module.
def inspect
"#<#{self.class.name} #{@description.inspect}>"
end
alias to_s inspect
# Ruby callback for when a module is included in another module is class.
# Our definition evaluates the shared group block in the context of the
# including example group.
def included(klass)
inclusion_line = klass.metadata[:location]
include_in klass, inclusion_line, [], nil
end
# @private
def include_in(klass, inclusion_line, args, customization_block)
klass.update_inherited_metadata(@metadata) unless @metadata.empty?
SharedExampleGroupInclusionStackFrame.with_frame(@description, inclusion_line) do
RSpec::Support::WithKeywordsWhenNeeded.class_exec(klass, *args, &@definition)
klass.class_exec(&customization_block) if customization_block
end
end
end
# Shared example groups let you define common context and/or common
# examples that you wish to use in multiple example groups.
#
# When defined, the shared group block is stored for later evaluation.
# It can later be included in an example group either explicitly
# (using `include_examples`, `include_context` or `it_behaves_like`)
# or implicitly (via matching metadata).
#
# Named shared example groups are scoped based on where they are
# defined. Shared groups defined in an example group are available
# for inclusion in that example group or any child example groups,
# but not in any parent or sibling example groups. Shared example
# groups defined at the top level can be included from any example group.
module SharedExampleGroup
# @overload shared_examples(name, &block)
# @param name [String, Symbol, Module] identifer to use when looking up
# this shared group
# @param block The block to be eval'd
# @overload shared_examples(name, metadata, &block)
# @param name [String, Symbol, Module] identifer to use when looking up
# this shared group
# @param metadata [Array<Symbol>, Hash] metadata to attach to this
# group; any example group or example with matching metadata will
# automatically include this shared example group.
# @param block The block to be eval'd
#
# Stores the block for later use. The block will be evaluated
# in the context of an example group via `include_examples`,
# `include_context`, or `it_behaves_like`.
#
# @example
# shared_examples "auditable" do
# it "stores an audit record on save!" do
# expect { auditable.save! }.to change(Audit, :count).by(1)
# end
# end
#
# RSpec.describe Account do
# it_behaves_like "auditable" do
# let(:auditable) { Account.new }
# end
# end
#
# @see ExampleGroup.it_behaves_like
# @see ExampleGroup.include_examples
# @see ExampleGroup.include_context
def shared_examples(name, *args, &block)
top_level = self == ExampleGroup
if top_level && RSpec::Support.thread_local_data[:in_example_group]
raise "Creating isolated shared examples from within a context is " \
"not allowed. Remove `RSpec.` prefix or move this to a " \
"top-level scope."
end
RSpec.world.shared_example_group_registry.add(self, name, *args, &block)
end
alias shared_context shared_examples
alias shared_examples_for shared_examples
# @api private
#
# Shared examples top level DSL.
module TopLevelDSL
# @private
def self.definitions
proc do
def shared_examples(name, *args, &block)
RSpec.world.shared_example_group_registry.add(:main, name, *args, &block)
end
alias shared_context shared_examples
alias shared_examples_for shared_examples
end
end
# @private
def self.exposed_globally?
@exposed_globally ||= false
end
# @api private
#
# Adds the top level DSL methods to Module and the top level binding.
def self.expose_globally!
return if exposed_globally?
Core::DSL.change_global_dsl(&definitions)
@exposed_globally = true
end
# @api private
#
# Removes the top level DSL methods to Module and the top level binding.
def self.remove_globally!
return unless exposed_globally?
Core::DSL.change_global_dsl do
undef shared_examples
undef shared_context
undef shared_examples_for
end
@exposed_globally = false
end
end
# @private
class Registry
def add(context, name, *metadata_args, &block)
unless block
RSpec.warning "Shared example group #{name} was defined without a "\
"block and will have no effect. Please define a "\
"block or remove the definition."
end
if RSpec.configuration.shared_context_metadata_behavior == :trigger_inclusion
return legacy_add(context, name, *metadata_args, &block)
end
unless valid_name?(name)
raise ArgumentError, "Shared example group names can only be a string, " \
"symbol or module but got: #{name.inspect}"
end
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
warn_if_key_taken context, name, block
metadata = Metadata.build_hash_from(metadata_args)
shared_module = SharedExampleGroupModule.new(name, block, metadata)
shared_example_groups[context][name] = shared_module
end
def find(lookup_contexts, name)
lookup_contexts.each do |context|
found = shared_example_groups[context][name]
return found if found
end
shared_example_groups[:main][name]
end
private
# TODO: remove this in RSpec 4. This exists only to support
# `config.shared_context_metadata_behavior == :trigger_inclusion`,
# the legacy behavior of shared context metadata, which we do
# not want to support in RSpec 4.
def legacy_add(context, name, *metadata_args, &block)
ensure_block_has_source_location(block) { CallerFilter.first_non_rspec_line }
shared_module = SharedExampleGroupModule.new(name, block, {})
if valid_name?(name)
warn_if_key_taken context, name, block
shared_example_groups[context][name] = shared_module
else
metadata_args.unshift name
end
return if metadata_args.empty?
RSpec.configuration.include shared_module, *metadata_args
end
def shared_example_groups
@shared_example_groups ||= Hash.new { |hash, context| hash[context] = {} }
end
def valid_name?(candidate)
case candidate
when String, Symbol, Module then true
else false
end
end
def warn_if_key_taken(context, key, new_block)
existing_module = shared_example_groups[context][key]
return unless existing_module
old_definition_location = formatted_location existing_module.definition
new_definition_location = formatted_location new_block
loaded_spec_files = RSpec.configuration.loaded_spec_files
if loaded_spec_files.include?(new_definition_location) && old_definition_location == new_definition_location
RSpec.warn_with <<-WARNING.gsub(/^ +\|/, ''), :call_site => nil
|WARNING: Your shared example group, '#{key}', defined at:
| #{old_definition_location}
|was automatically loaded by RSpec because the file name
|matches the configured autoloading pattern (#{RSpec.configuration.pattern}),
|and is also being required from somewhere else. To fix this
|warning, either rename the file to not match the pattern, or
|do not explicitly require the file.
WARNING
else
RSpec.warn_with <<-WARNING.gsub(/^ +\|/, ''), :call_site => nil
|WARNING: Shared example group '#{key}' has been previously defined at:
| #{old_definition_location}
|...and you are now defining it at:
| #{new_definition_location}
|The new definition will overwrite the original one.
WARNING
end
end
if RUBY_VERSION.to_f >= 1.9
def formatted_location(block)
block.source_location.join(":")
end
else # 1.8.7
# :nocov:
def formatted_location(block)
block.source_location.join(":").gsub(/:in.*$/, '')
end
# :nocov:
end
if Proc.method_defined?(:source_location)
def ensure_block_has_source_location(_block); end
else # for 1.8.7
# :nocov:
def ensure_block_has_source_location(block)
source_location = yield.split(':')
block.extend(Module.new { define_method(:source_location) { source_location } })
end
# :nocov:
end
end
end
end
instance_exec(&Core::SharedExampleGroup::TopLevelDSL.definitions)
end