crspec 0.1.2 → 0.1.3
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
- data/CHANGELOG.md +86 -0
- data/README.md +1 -1
- data/lib/crspec/cli.rb +86 -5
- data/lib/crspec/configuration.rb +62 -9
- data/lib/crspec/dsl.rb +66 -2
- data/lib/crspec/example.rb +60 -6
- data/lib/crspec/example_group.rb +363 -63
- data/lib/crspec/execution_context.rb +10 -6
- data/lib/crspec/expectations.rb +62 -2
- data/lib/crspec/file_fixtures.rb +18 -0
- data/lib/crspec/formatters/progress_formatter.rb +37 -14
- data/lib/crspec/matchers.rb +642 -0
- data/lib/crspec/mock/argument_matchers.rb +157 -0
- data/lib/crspec/mock/double.rb +246 -13
- data/lib/crspec/mock/interceptor.rb +32 -9
- data/lib/crspec/mock/space.rb +14 -0
- data/lib/crspec/process_runner.rb +332 -0
- data/lib/crspec/rails/assets_shim.rb +33 -0
- data/lib/crspec/rails/database_isolation.rb +179 -8
- data/lib/crspec/rails/parallel.rb +29 -29
- data/lib/crspec/rails/request_helpers.rb +70 -14
- data/lib/crspec/rails/system_server.rb +7 -0
- data/lib/crspec/rails/warden_shim.rb +18 -0
- data/lib/crspec/runner.rb +143 -29
- data/lib/crspec/shared_examples.rb +70 -0
- data/lib/crspec/status_persistence.rb +46 -0
- data/lib/crspec/transpiler/cli.rb +106 -15
- data/lib/crspec/transpiler/rewriter.rb +179 -25
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +6 -0
- data/lib/rspec/core.rb +2 -0
- data/lib/rspec/expectations.rb +2 -0
- data/lib/rspec/mocks.rb +2 -0
- data/lib/rspec/rails.rb +2 -0
- data/lib/rspec.rb +2 -0
- metadata +27 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crspec
|
|
4
|
+
module Mock
|
|
5
|
+
# RSpec-style argument matchers usable in with(...) and have_received
|
|
6
|
+
# constraints. Each responds to ===(actual).
|
|
7
|
+
module ArgumentMatchers
|
|
8
|
+
class Anything
|
|
9
|
+
def ===(_other) = true
|
|
10
|
+
def ==(other) = true
|
|
11
|
+
def inspect = "anything"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class HashIncluding
|
|
15
|
+
def initialize(expected)
|
|
16
|
+
@expected = expected
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ===(actual)
|
|
20
|
+
return false unless actual.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
@expected.all? { |k, v| actual.key?(k) && (v == actual[k] || v === actual[k]) }
|
|
23
|
+
end
|
|
24
|
+
alias == ===
|
|
25
|
+
|
|
26
|
+
def inspect = "hash_including(#{@expected.inspect})"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class HashExcluding
|
|
30
|
+
def initialize(expected)
|
|
31
|
+
@expected = expected
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ===(actual)
|
|
35
|
+
return false unless actual.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
@expected.none? { |k, v| actual.key?(k) && (v == actual[k] || v === actual[k]) }
|
|
38
|
+
end
|
|
39
|
+
alias == ===
|
|
40
|
+
|
|
41
|
+
def inspect = "hash_excluding(#{@expected.inspect})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class ArrayIncluding
|
|
45
|
+
def initialize(expected)
|
|
46
|
+
@expected = expected
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ===(actual)
|
|
50
|
+
return false unless actual.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
@expected.all? { |item| actual.any? { |el| item == el || item === el } }
|
|
53
|
+
end
|
|
54
|
+
alias == ===
|
|
55
|
+
|
|
56
|
+
def inspect = "array_including(#{@expected.inspect})"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class InstanceOf
|
|
60
|
+
def initialize(klass)
|
|
61
|
+
@klass = klass
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def ===(actual) = actual.instance_of?(@klass)
|
|
65
|
+
alias == ===
|
|
66
|
+
|
|
67
|
+
def inspect = "an_instance_of(#{@klass})"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class KindOf
|
|
71
|
+
def initialize(klass)
|
|
72
|
+
@klass = klass
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ===(actual) = actual.is_a?(@klass)
|
|
76
|
+
alias == ===
|
|
77
|
+
|
|
78
|
+
def inspect = "kind_of(#{@klass})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Duck
|
|
82
|
+
def initialize(*methods)
|
|
83
|
+
@methods = methods
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def ===(actual) = @methods.all? { |m| actual.respond_to?(m) }
|
|
87
|
+
alias == ===
|
|
88
|
+
|
|
89
|
+
def inspect = "duck_type(#{@methods.map(&:inspect).join(", ")})"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def anything
|
|
93
|
+
Anything.new
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def hash_including(expected = {}, **kwargs)
|
|
97
|
+
HashIncluding.new(expected.merge(kwargs))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def hash_excluding(expected = {}, **kwargs)
|
|
101
|
+
HashExcluding.new(expected.merge(kwargs))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def array_including(*items)
|
|
105
|
+
ArrayIncluding.new(items.flatten(1))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def an_instance_of(klass)
|
|
109
|
+
InstanceOf.new(klass)
|
|
110
|
+
end
|
|
111
|
+
alias instance_of an_instance_of
|
|
112
|
+
|
|
113
|
+
def kind_of(klass)
|
|
114
|
+
KindOf.new(klass)
|
|
115
|
+
end
|
|
116
|
+
alias a_kind_of kind_of
|
|
117
|
+
|
|
118
|
+
def duck_type(*methods)
|
|
119
|
+
Duck.new(*methods)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.args_match?(expected_args, actual_args, actual_kwargs = nil)
|
|
123
|
+
return true if expected_args.nil?
|
|
124
|
+
|
|
125
|
+
# A trailing hash matcher (hash_including etc.) may target the
|
|
126
|
+
# call's keyword arguments.
|
|
127
|
+
if actual_kwargs && !actual_kwargs.empty? &&
|
|
128
|
+
expected_args.size == actual_args.size + 1 &&
|
|
129
|
+
(expected_args.last.is_a?(HashIncluding) || expected_args.last.is_a?(HashExcluding) || expected_args.last.is_a?(Hash))
|
|
130
|
+
positional = expected_args[0...-1]
|
|
131
|
+
return args_match?(positional, actual_args) &&
|
|
132
|
+
(expected_args.last == actual_kwargs || expected_args.last === actual_kwargs)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
return false unless expected_args.size == actual_args.size
|
|
136
|
+
|
|
137
|
+
expected_args.zip(actual_args).all? do |expected, actual|
|
|
138
|
+
expected == actual || expected === actual
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.kwargs_match?(expected_kwargs, actual_kwargs)
|
|
143
|
+
return true if expected_kwargs.nil? || expected_kwargs.empty?
|
|
144
|
+
|
|
145
|
+
if expected_kwargs.size == 1 && expected_kwargs.values.first.is_a?(HashIncluding)
|
|
146
|
+
return expected_kwargs.values.first === actual_kwargs
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return false unless expected_kwargs.size == actual_kwargs.size
|
|
150
|
+
|
|
151
|
+
expected_kwargs.all? do |key, expected|
|
|
152
|
+
actual_kwargs.key?(key) && (expected == actual_kwargs[key] || expected === actual_kwargs[key])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/crspec/mock/double.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "space"
|
|
4
|
+
require_relative "argument_matchers"
|
|
4
5
|
|
|
5
6
|
module Crspec
|
|
6
7
|
module Mock
|
|
@@ -13,10 +14,15 @@ module Crspec
|
|
|
13
14
|
@name = name || "Double"
|
|
14
15
|
Space.current.register_double(self)
|
|
15
16
|
stubs.each do |method_name, return_value|
|
|
17
|
+
__crspec_verify_stub!(method_name)
|
|
16
18
|
Space.current.register_stub(self, method_name, proc { return_value })
|
|
17
19
|
end
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def __crspec_verify_stub!(method_name)
|
|
23
|
+
# Plain doubles accept any stub.
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def verify_expectations!
|
|
21
27
|
# Verified via Space
|
|
22
28
|
end
|
|
@@ -26,19 +32,47 @@ module Crspec
|
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# A verifying double: only methods that exist on the doubled class's
|
|
36
|
+
# instance interface may be stubbed or expected.
|
|
37
|
+
class InstanceDouble < Double
|
|
38
|
+
def initialize(doubled_class, name = nil, stubs = {})
|
|
39
|
+
@doubled_class = doubled_class.is_a?(Module) ? doubled_class : nil
|
|
40
|
+
@doubled_class_name = doubled_class.is_a?(Module) ? doubled_class.name : doubled_class.to_s
|
|
41
|
+
super(name || @doubled_class_name, stubs)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def __crspec_verify_stub!(method_name)
|
|
45
|
+
return unless @doubled_class
|
|
46
|
+
|
|
47
|
+
method_sym = method_name.to_sym
|
|
48
|
+
return if @doubled_class.method_defined?(method_sym) ||
|
|
49
|
+
@doubled_class.private_method_defined?(method_sym)
|
|
50
|
+
|
|
51
|
+
raise MockError,
|
|
52
|
+
"the #{@doubled_class_name} class does not implement the instance method: #{method_sym}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def inspect
|
|
56
|
+
"#<InstanceDouble(#{@doubled_class_name}) #{@name.inspect}>"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
29
60
|
class StubChain
|
|
30
61
|
attr_reader :target, :method_name
|
|
31
62
|
|
|
32
63
|
def initialize(target, method_name)
|
|
33
64
|
@target = target
|
|
34
65
|
@method_name = method_name.to_sym
|
|
66
|
+
target.__crspec_verify_stub!(@method_name) if target.respond_to?(:__crspec_verify_stub!)
|
|
35
67
|
@expected_args = nil
|
|
68
|
+
@expected_kwargs = nil
|
|
36
69
|
@return_proc = proc {}
|
|
37
70
|
register_default_stub
|
|
38
71
|
end
|
|
39
72
|
|
|
40
|
-
def with(*args)
|
|
73
|
+
def with(*args, **kwargs)
|
|
41
74
|
@expected_args = args
|
|
75
|
+
@expected_kwargs = kwargs
|
|
42
76
|
update_stub
|
|
43
77
|
self
|
|
44
78
|
end
|
|
@@ -78,6 +112,23 @@ module Crspec
|
|
|
78
112
|
self
|
|
79
113
|
end
|
|
80
114
|
|
|
115
|
+
def and_call_original
|
|
116
|
+
target = @target
|
|
117
|
+
method_name = @method_name
|
|
118
|
+
original = begin
|
|
119
|
+
target.method(method_name)
|
|
120
|
+
rescue NameError
|
|
121
|
+
raise MockError, "#{target.inspect} has no original implementation of #{method_name}"
|
|
122
|
+
end
|
|
123
|
+
# Find the implementation beneath the interceptor.
|
|
124
|
+
original = original.super_method while original && original.owner == Interceptor
|
|
125
|
+
raise MockError, "#{target.inspect} has no original implementation of #{method_name}" unless original
|
|
126
|
+
|
|
127
|
+
@return_proc = proc { |*args, **kwargs, &block| original.call(*args, **kwargs, &block) }
|
|
128
|
+
update_stub
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
81
132
|
private
|
|
82
133
|
|
|
83
134
|
def register_default_stub
|
|
@@ -88,13 +139,19 @@ module Crspec
|
|
|
88
139
|
target = @target
|
|
89
140
|
method_name = @method_name
|
|
90
141
|
expected_args = @expected_args
|
|
142
|
+
expected_kwargs = @expected_kwargs
|
|
91
143
|
return_proc = @return_proc
|
|
92
144
|
|
|
93
145
|
implementation = proc do |*args, **kwargs, &block|
|
|
94
|
-
|
|
146
|
+
Space.current.record_call(target, method_name, args, kwargs)
|
|
147
|
+
if expected_args && !expected_args.empty? && !ArgumentMatchers.args_match?(expected_args, args, kwargs)
|
|
95
148
|
raise MockError, "Expected #{method_name} with #{expected_args.inspect}, got #{args.inspect}"
|
|
96
149
|
end
|
|
97
150
|
|
|
151
|
+
if expected_kwargs && !expected_kwargs.empty? && !ArgumentMatchers.kwargs_match?(expected_kwargs, kwargs)
|
|
152
|
+
raise MockError, "Expected #{method_name} with #{expected_kwargs.inspect}, got #{kwargs.inspect}"
|
|
153
|
+
end
|
|
154
|
+
|
|
98
155
|
return_proc.call(*args, **kwargs, &block)
|
|
99
156
|
end
|
|
100
157
|
|
|
@@ -153,14 +210,20 @@ module Crspec
|
|
|
153
210
|
target = @target
|
|
154
211
|
method_name = @method_name
|
|
155
212
|
expected_args = @expected_args
|
|
213
|
+
expected_kwargs = @expected_kwargs
|
|
156
214
|
return_proc = @return_proc
|
|
157
215
|
|
|
158
216
|
implementation = proc do |*args, **kwargs, &block|
|
|
159
217
|
@call_count += 1
|
|
160
|
-
|
|
218
|
+
Space.current.record_call(target, method_name, args, kwargs)
|
|
219
|
+
if expected_args && !expected_args.empty? && !ArgumentMatchers.args_match?(expected_args, args, kwargs)
|
|
161
220
|
raise MockError, "Expected #{method_name} with #{expected_args.inspect}, got #{args.inspect}"
|
|
162
221
|
end
|
|
163
222
|
|
|
223
|
+
if expected_kwargs && !expected_kwargs.empty? && !ArgumentMatchers.kwargs_match?(expected_kwargs, kwargs)
|
|
224
|
+
raise MockError, "Expected #{method_name} with #{expected_kwargs.inspect}, got #{kwargs.inspect}"
|
|
225
|
+
end
|
|
226
|
+
|
|
164
227
|
return_proc.call(*args, **kwargs, &block)
|
|
165
228
|
end
|
|
166
229
|
|
|
@@ -174,6 +237,7 @@ module Crspec
|
|
|
174
237
|
def initialize(method_name)
|
|
175
238
|
@method_name = method_name.to_sym
|
|
176
239
|
@expected_args = nil
|
|
240
|
+
@expected_kwargs = nil
|
|
177
241
|
@return_values = nil
|
|
178
242
|
@raise_exception = nil
|
|
179
243
|
@yield_args = nil
|
|
@@ -181,8 +245,9 @@ module Crspec
|
|
|
181
245
|
@count_constraint = :exact
|
|
182
246
|
end
|
|
183
247
|
|
|
184
|
-
def with(*args)
|
|
248
|
+
def with(*args, **kwargs)
|
|
185
249
|
@expected_args = args
|
|
250
|
+
@expected_kwargs = kwargs
|
|
186
251
|
self
|
|
187
252
|
end
|
|
188
253
|
|
|
@@ -202,6 +267,11 @@ module Crspec
|
|
|
202
267
|
self
|
|
203
268
|
end
|
|
204
269
|
|
|
270
|
+
def and_call_original
|
|
271
|
+
@call_original = true
|
|
272
|
+
self
|
|
273
|
+
end
|
|
274
|
+
|
|
205
275
|
def once
|
|
206
276
|
@expected_count = 1
|
|
207
277
|
@count_constraint = :exact
|
|
@@ -243,10 +313,32 @@ module Crspec
|
|
|
243
313
|
private
|
|
244
314
|
|
|
245
315
|
def apply_chain_options(chain)
|
|
246
|
-
|
|
316
|
+
if @expected_args || @expected_kwargs
|
|
317
|
+
chain.with(*(@expected_args || []), **(@expected_kwargs || {}))
|
|
318
|
+
end
|
|
247
319
|
chain.and_return(*@return_values) if @return_values
|
|
248
320
|
chain.and_raise(@raise_exception, @raise_message) if @raise_exception
|
|
249
321
|
chain.and_yield(*@yield_args) if @yield_args
|
|
322
|
+
chain.and_call_original if @call_original
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# allow(target).to receive_messages(a: 1, b: 2)
|
|
327
|
+
class ReceiveMessagesMatcher
|
|
328
|
+
def initialize(messages)
|
|
329
|
+
@messages = messages
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def setup_allow(target)
|
|
333
|
+
@messages.map do |method_name, value|
|
|
334
|
+
StubChain.new(target, method_name).and_return(value)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def setup_expect(target)
|
|
339
|
+
@messages.map do |method_name, value|
|
|
340
|
+
ExpectationChain.new(target, method_name).and_return(value)
|
|
341
|
+
end
|
|
250
342
|
end
|
|
251
343
|
end
|
|
252
344
|
|
|
@@ -264,20 +356,131 @@ module Crspec
|
|
|
264
356
|
end
|
|
265
357
|
end
|
|
266
358
|
|
|
359
|
+
# expect(target).to have_received(:method).with(...).once
|
|
360
|
+
class HaveReceivedMatcher
|
|
361
|
+
def initialize(method_name)
|
|
362
|
+
@method_name = method_name.to_sym
|
|
363
|
+
@expected_args = nil
|
|
364
|
+
@expected_kwargs = nil
|
|
365
|
+
@expected_count = nil
|
|
366
|
+
@count_constraint = :at_least_once
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def with(*args, **kwargs)
|
|
370
|
+
@expected_args = args
|
|
371
|
+
@expected_kwargs = kwargs
|
|
372
|
+
self
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def once
|
|
376
|
+
@expected_count = 1
|
|
377
|
+
@count_constraint = :exact
|
|
378
|
+
self
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def twice
|
|
382
|
+
@expected_count = 2
|
|
383
|
+
@count_constraint = :exact
|
|
384
|
+
self
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def exactly(n)
|
|
388
|
+
@expected_count = n
|
|
389
|
+
@count_constraint = :exact
|
|
390
|
+
self
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def times
|
|
394
|
+
self
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def at_least(n)
|
|
398
|
+
@expected_count = n
|
|
399
|
+
@count_constraint = :at_least
|
|
400
|
+
self
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def matches?(target)
|
|
404
|
+
@target = target
|
|
405
|
+
calls = Space.current.calls_for(target, @method_name)
|
|
406
|
+
if @expected_args || @expected_kwargs
|
|
407
|
+
calls = calls.select do |args, kwargs|
|
|
408
|
+
ArgumentMatchers.args_match?(@expected_args, args, kwargs) &&
|
|
409
|
+
ArgumentMatchers.kwargs_match?(@expected_kwargs, kwargs)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
@actual_count = calls.size
|
|
413
|
+
|
|
414
|
+
case @count_constraint
|
|
415
|
+
when :exact then @actual_count == @expected_count
|
|
416
|
+
when :at_least then @actual_count >= @expected_count
|
|
417
|
+
else @actual_count.positive?
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def failure_message
|
|
422
|
+
expectation = case @count_constraint
|
|
423
|
+
when :exact then "exactly #{@expected_count} time(s)"
|
|
424
|
+
when :at_least then "at least #{@expected_count} time(s)"
|
|
425
|
+
else "at least once"
|
|
426
|
+
end
|
|
427
|
+
with_clause = @expected_args ? " with #{@expected_args.inspect}" : ""
|
|
428
|
+
"Expected #{@target.inspect} to have received #{@method_name.inspect}#{with_clause} #{expectation}, " \
|
|
429
|
+
"but received it #{@actual_count} time(s)"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def failure_message_when_negated
|
|
433
|
+
"Expected #{@target.inspect} not to have received #{@method_name.inspect}, " \
|
|
434
|
+
"but received it #{@actual_count} time(s)"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# A spy: a double that accepts any message and records calls for
|
|
439
|
+
# have_received verification.
|
|
440
|
+
class Spy < Double
|
|
441
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
442
|
+
space = Fiber[Space::STORAGE_KEY]
|
|
443
|
+
if space
|
|
444
|
+
if (stub = space.fetch_stub(self, method_name))
|
|
445
|
+
return stub.call(*args, **kwargs, &block)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
space.record_call(self, method_name, args, kwargs)
|
|
449
|
+
end
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
454
|
+
true
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def inspect
|
|
458
|
+
"#<Spy #{@name.inspect}>"
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
267
462
|
module DSL
|
|
463
|
+
include ArgumentMatchers
|
|
464
|
+
|
|
268
465
|
def double(name = nil, stubs = {})
|
|
269
466
|
Double.new(name, stubs)
|
|
270
467
|
end
|
|
271
468
|
|
|
272
469
|
def instance_double(target_class, name = nil, stubs = {})
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
470
|
+
if name.is_a?(Hash) && stubs.empty?
|
|
471
|
+
stubs = name
|
|
472
|
+
name = nil
|
|
473
|
+
end
|
|
474
|
+
resolved = if target_class.is_a?(String) || target_class.is_a?(Symbol)
|
|
475
|
+
begin
|
|
476
|
+
Object.const_get(target_class.to_s)
|
|
477
|
+
rescue NameError
|
|
478
|
+
target_class.to_s
|
|
479
|
+
end
|
|
480
|
+
else
|
|
481
|
+
target_class
|
|
482
|
+
end
|
|
483
|
+
InstanceDouble.new(resolved, name, stubs)
|
|
281
484
|
end
|
|
282
485
|
|
|
283
486
|
def allow(target)
|
|
@@ -287,6 +490,36 @@ module Crspec
|
|
|
287
490
|
def receive(method_name)
|
|
288
491
|
ReceiveMatcher.new(method_name)
|
|
289
492
|
end
|
|
493
|
+
|
|
494
|
+
def receive_messages(messages)
|
|
495
|
+
ReceiveMessagesMatcher.new(messages)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def spy(name = nil, stubs = {})
|
|
499
|
+
Spy.new(name || "Spy", stubs)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def have_received(method_name)
|
|
503
|
+
HaveReceivedMatcher.new(method_name)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def allow_any_instance_of(_klass)
|
|
507
|
+
raise Crspec::MigrationError, <<~MSG
|
|
508
|
+
allow_any_instance_of is not supported by Crspec: it mutates shared
|
|
509
|
+
class hierarchies at runtime, which is unsafe under concurrent
|
|
510
|
+
threads/fibers. Inject a double or stub the specific instance.
|
|
511
|
+
Run `crspec-transpile --analyze` to find all occurrences.
|
|
512
|
+
MSG
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def expect_any_instance_of(_klass)
|
|
516
|
+
raise Crspec::MigrationError, <<~MSG
|
|
517
|
+
expect_any_instance_of is not supported by Crspec: it mutates shared
|
|
518
|
+
class hierarchies at runtime, which is unsafe under concurrent
|
|
519
|
+
threads/fibers. Inject a double or stub the specific instance.
|
|
520
|
+
Run `crspec-transpile --analyze` to find all occurrences.
|
|
521
|
+
MSG
|
|
522
|
+
end
|
|
290
523
|
end
|
|
291
524
|
end
|
|
292
525
|
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module Crspec
|
|
4
4
|
module Mock
|
|
5
5
|
module Interceptor
|
|
6
|
+
@intercepted_methods = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
|
|
6
9
|
def method_missing(method_name, *args, **kwargs, &block)
|
|
7
10
|
space = Fiber[Space::STORAGE_KEY]
|
|
8
11
|
if space && (stub = space.fetch_stub(self, method_name))
|
|
@@ -17,16 +20,36 @@ module Crspec
|
|
|
17
20
|
space&.fetch_stub(self, method_name) || super
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
class << self
|
|
24
|
+
def add_intercept_method(method_name)
|
|
25
|
+
method_sym = method_name.to_sym
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
return if @intercepted_methods.key?(method_sym)
|
|
28
|
+
return if instance_methods(false).include?(method_sym) ||
|
|
29
|
+
private_instance_methods(false).include?(method_sym)
|
|
30
|
+
|
|
31
|
+
define_method(method_sym) do |*args, **kwargs, &block|
|
|
32
|
+
space = Fiber[Space::STORAGE_KEY]
|
|
33
|
+
if space && (stub = space.fetch_stub(self, method_sym))
|
|
34
|
+
stub.call(*args, **kwargs, &block)
|
|
35
|
+
else
|
|
36
|
+
super(*args, **kwargs, &block)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
@intercepted_methods[method_sym] = true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
# Removes all dynamically-defined intercept methods so they do not
|
|
44
|
+
# accumulate for the lifetime of the process (e.g. across multiple
|
|
45
|
+
# embedded runner invocations). Stub lookups fall back to
|
|
46
|
+
# method_missing until re-added.
|
|
47
|
+
def cleanup!
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
@intercepted_methods.each_key do |method_sym|
|
|
50
|
+
remove_method(method_sym) if instance_methods(false).include?(method_sym)
|
|
51
|
+
end
|
|
52
|
+
@intercepted_methods.clear
|
|
30
53
|
end
|
|
31
54
|
end
|
|
32
55
|
end
|
data/lib/crspec/mock/space.rb
CHANGED
|
@@ -27,9 +27,22 @@ module Crspec
|
|
|
27
27
|
@doubles = []
|
|
28
28
|
@stubs = Hash.new { |h, k| h[k] = {} }
|
|
29
29
|
@expectations = []
|
|
30
|
+
@calls = Hash.new { |h, k| h[k] = [] }
|
|
30
31
|
@mutex = Mutex.new
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def record_call(target, method_name, args, kwargs)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@calls[[target.object_id, method_name.to_sym]] << [args, kwargs]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def calls_for(target, method_name)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@calls[[target.object_id, method_name.to_sym]].dup
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
33
46
|
def register_stub(target, method_name, implementation)
|
|
34
47
|
method_sym = method_name.to_sym
|
|
35
48
|
ensure_interceptor_prepended(target, method_sym)
|
|
@@ -66,6 +79,7 @@ module Crspec
|
|
|
66
79
|
@stubs.clear
|
|
67
80
|
@doubles.clear
|
|
68
81
|
@expectations.clear
|
|
82
|
+
@calls.clear
|
|
69
83
|
end
|
|
70
84
|
end
|
|
71
85
|
|