crspec 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +13 -0
- data/demo.rb +126 -0
- data/exe/crspec +9 -0
- data/exe/crspec-transpile +8 -0
- data/lib/crspec/cli.rb +82 -0
- data/lib/crspec/dsl.rb +39 -0
- data/lib/crspec/example.rb +53 -0
- data/lib/crspec/example_group.rb +154 -0
- data/lib/crspec/execution_context.rb +62 -0
- data/lib/crspec/expectations.rb +44 -0
- data/lib/crspec/formatters/progress_formatter.rb +50 -0
- data/lib/crspec/matchers.rb +203 -0
- data/lib/crspec/mock/double.rb +285 -0
- data/lib/crspec/mock/interceptor.rb +35 -0
- data/lib/crspec/mock/space.rb +81 -0
- data/lib/crspec/rails/database_isolation.rb +29 -0
- data/lib/crspec/rails/parallel.rb +98 -0
- data/lib/crspec/rails/system_server.rb +35 -0
- data/lib/crspec/runner.rb +97 -0
- data/lib/crspec/transpiler/cli.rb +108 -0
- data/lib/crspec/transpiler/rewriter.rb +70 -0
- data/lib/crspec/version.rb +5 -0
- data/lib/crspec.rb +23 -0
- data/samples/user_spec.rb +31 -0
- data/sig/crspec.rbs +4 -0
- data/test/crspec/execution_context_test.rb +61 -0
- data/test/crspec/mock_test.rb +55 -0
- data/test/crspec/rails_test.rb +62 -0
- data/test/crspec/runner_test.rb +68 -0
- data/test/crspec/transpiler_test.rb +41 -0
- data/test/test_helper.rb +6 -0
- metadata +96 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module Crspec
|
|
7
|
+
class ExecutionContext
|
|
8
|
+
STORAGE_KEY = :crspec_execution_context
|
|
9
|
+
|
|
10
|
+
attr_reader :example_id, :metadata, :parent
|
|
11
|
+
|
|
12
|
+
def self.current
|
|
13
|
+
Fiber[STORAGE_KEY] ||= new(SecureRandom.uuid)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.isolate(example_id, metadata = {})
|
|
17
|
+
parent_context = Fiber[STORAGE_KEY]
|
|
18
|
+
new_context = new(example_id, metadata, parent_context)
|
|
19
|
+
|
|
20
|
+
# Fiber storage inheritance preserves context while isolating mutations
|
|
21
|
+
Fiber.new(storage: { STORAGE_KEY => new_context }) do
|
|
22
|
+
yield new_context
|
|
23
|
+
end.resume
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(example_id, metadata = {}, parent = nil)
|
|
27
|
+
@example_id = example_id
|
|
28
|
+
@metadata = metadata.freeze
|
|
29
|
+
@parent = parent
|
|
30
|
+
@memoized_values = {}
|
|
31
|
+
@monitor = Monitor.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fetch_memoized(key, &block)
|
|
35
|
+
return @memoized_values[key] if @memoized_values.key?(key)
|
|
36
|
+
|
|
37
|
+
@monitor.synchronize do
|
|
38
|
+
return @memoized_values[key] if @memoized_values.key?(key)
|
|
39
|
+
|
|
40
|
+
@memoized_values[key] = block.call
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def [](key)
|
|
45
|
+
@monitor.synchronize do
|
|
46
|
+
@memoized_values[key]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def []=(key, value)
|
|
51
|
+
@monitor.synchronize do
|
|
52
|
+
@memoized_values[key] = value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reset!
|
|
57
|
+
@monitor.synchronize do
|
|
58
|
+
@memoized_values.clear
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "matchers"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
class ExpectationNotMetError < StandardError; end
|
|
7
|
+
|
|
8
|
+
class ExpectationTarget
|
|
9
|
+
def initialize(actual)
|
|
10
|
+
@actual = actual
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to(matcher = nil, &block)
|
|
14
|
+
matcher ||= block
|
|
15
|
+
return matcher.setup_expect(@actual) if matcher.respond_to?(:setup_expect)
|
|
16
|
+
|
|
17
|
+
raise ExpectationNotMetError, matcher.failure_message unless matcher.matches?(@actual)
|
|
18
|
+
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def not_to(matcher = nil, &block)
|
|
23
|
+
matcher ||= block
|
|
24
|
+
if matcher.matches?(@actual)
|
|
25
|
+
msg = matcher.respond_to?(:failure_message_when_negated) ? matcher.failure_message_when_negated : "Expected matcher not to match"
|
|
26
|
+
raise ExpectationNotMetError, msg
|
|
27
|
+
end
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias to_not not_to
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module Expectations
|
|
35
|
+
include Matchers
|
|
36
|
+
|
|
37
|
+
UNDEFINED = Object.new.freeze
|
|
38
|
+
|
|
39
|
+
def expect(value = UNDEFINED, &block)
|
|
40
|
+
actual = value.equal?(UNDEFINED) ? block : value
|
|
41
|
+
ExpectationTarget.new(actual)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crspec
|
|
4
|
+
module Formatters
|
|
5
|
+
class ProgressFormatter
|
|
6
|
+
def initialize(output = $stdout, color: output.respond_to?(:tty?) && output.tty?)
|
|
7
|
+
@output = output
|
|
8
|
+
@color = color
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def example_passed(_example)
|
|
13
|
+
@mutex.synchronize do
|
|
14
|
+
@output.print @color ? "\e[32m.\e[0m" : "."
|
|
15
|
+
@output.flush
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def example_failed(_example)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
@output.print @color ? "\e[31mF\e[0m" : "F"
|
|
22
|
+
@output.flush
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def example_pending(_example)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
@output.print @color ? "\e[33m*\e[0m" : "*"
|
|
29
|
+
@output.flush
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start; end
|
|
34
|
+
|
|
35
|
+
def finish
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@output.puts
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class NullFormatter
|
|
43
|
+
def example_passed(_example); end
|
|
44
|
+
def example_failed(_example); end
|
|
45
|
+
def example_pending(_example); end
|
|
46
|
+
def start; end
|
|
47
|
+
def finish; end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crspec
|
|
4
|
+
module Matchers
|
|
5
|
+
class BaseMatcher
|
|
6
|
+
attr_reader :actual, :expected
|
|
7
|
+
|
|
8
|
+
def initialize(expected = nil)
|
|
9
|
+
@expected = expected
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def failure_message
|
|
13
|
+
"Expected #{@actual.inspect} to match #{@expected.inspect}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failure_message_when_negated
|
|
17
|
+
"Expected #{@actual.inspect} not to match #{@expected.inspect}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class EqMatcher < BaseMatcher
|
|
22
|
+
def matches?(actual)
|
|
23
|
+
@actual = actual
|
|
24
|
+
@actual == @expected
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failure_message
|
|
28
|
+
"Expected #{@expected.inspect}, got #{@actual.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failure_message_when_negated
|
|
32
|
+
"Expected value not to equal #{@expected.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class BeMatcher < BaseMatcher
|
|
37
|
+
def matches?(actual)
|
|
38
|
+
@actual = actual
|
|
39
|
+
if @expected.nil? && !instance_variable_defined?(:@expected)
|
|
40
|
+
true
|
|
41
|
+
elsif @expected.equal?(true) || @expected.equal?(false)
|
|
42
|
+
@actual == @expected
|
|
43
|
+
else
|
|
44
|
+
@actual.equal?(@expected)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def failure_message
|
|
49
|
+
"Expected #{@expected.inspect} (object_id: #{@expected.object_id}), got #{@actual.inspect} (object_id: #{@actual.object_id})"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class BeNilMatcher < BaseMatcher
|
|
54
|
+
def matches?(actual)
|
|
55
|
+
@actual = actual
|
|
56
|
+
@actual.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def failure_message
|
|
60
|
+
"Expected #{@actual.inspect} to be nil"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def failure_message_when_negated
|
|
64
|
+
"Expected #{@actual.inspect} not to be nil"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class BeTruthyMatcher < BaseMatcher
|
|
69
|
+
def matches?(actual)
|
|
70
|
+
@actual = actual
|
|
71
|
+
!!@actual
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def failure_message
|
|
75
|
+
"Expected #{@actual.inspect} to be truthy"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class BeFalsyMatcher < BaseMatcher
|
|
80
|
+
def matches?(actual)
|
|
81
|
+
@actual = actual
|
|
82
|
+
!@actual
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def failure_message
|
|
86
|
+
"Expected #{@actual.inspect} to be falsy"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class IncludeMatcher < BaseMatcher
|
|
91
|
+
def matches?(actual)
|
|
92
|
+
@actual = actual
|
|
93
|
+
if @expected.is_a?(Array)
|
|
94
|
+
@expected.all? { |e| @actual.include?(e) }
|
|
95
|
+
else
|
|
96
|
+
@actual.include?(@expected)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def failure_message
|
|
101
|
+
"Expected #{@actual.inspect} to include #{@expected.inspect}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure_message_when_negated
|
|
105
|
+
"Expected #{@actual.inspect} not to include #{@expected.inspect}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class RaiseErrorMatcher < BaseMatcher
|
|
110
|
+
def initialize(expected_exception = StandardError, expected_message = nil)
|
|
111
|
+
super(expected_exception)
|
|
112
|
+
@expected_exception = expected_exception
|
|
113
|
+
@expected_message = expected_message
|
|
114
|
+
@actual_exception = nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def matches?(block)
|
|
118
|
+
raise ArgumentError, "raise_error matcher requires a block" unless block.respond_to?(:call)
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
block.call
|
|
122
|
+
false
|
|
123
|
+
rescue Exception => e
|
|
124
|
+
@actual_exception = e
|
|
125
|
+
matches_exception?(e)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def failure_message
|
|
130
|
+
if @actual_exception
|
|
131
|
+
"Expected #{@expected_exception}#{@expected_message ? " with message #{@expected_message.inspect}" : ""}, but got #{@actual_exception.class}: #{@actual_exception.message.inspect}"
|
|
132
|
+
else
|
|
133
|
+
"Expected #{@expected_exception} to be raised, but nothing was raised"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def failure_message_when_negated
|
|
138
|
+
"Expected no exception, but #{@actual_exception.class} was raised"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def matches_exception?(e)
|
|
144
|
+
return false unless e.is_a?(@expected_exception)
|
|
145
|
+
return true unless @expected_message
|
|
146
|
+
|
|
147
|
+
if @expected_message.is_a?(Regexp)
|
|
148
|
+
@expected_message.match?(e.message)
|
|
149
|
+
else
|
|
150
|
+
e.message.include?(@expected_message.to_s)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class RespondToMatcher < BaseMatcher
|
|
156
|
+
def initialize(*methods)
|
|
157
|
+
super(methods)
|
|
158
|
+
@methods = methods
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def matches?(actual)
|
|
162
|
+
@actual = actual
|
|
163
|
+
@methods.all? { |m| @actual.respond_to?(m) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def failure_message
|
|
167
|
+
"Expected #{@actual.inspect} to respond to #{@methods.map(&:inspect).join(", ")}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def eq(expected)
|
|
172
|
+
EqMatcher.new(expected)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def be(expected = nil)
|
|
176
|
+
BeMatcher.new(expected)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def be_nil
|
|
180
|
+
BeNilMatcher.new
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def be_truthy
|
|
184
|
+
BeTruthyMatcher.new
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def be_falsy
|
|
188
|
+
BeFalsyMatcher.new
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def include(*expected)
|
|
192
|
+
IncludeMatcher.new(expected.size == 1 ? expected.first : expected)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def raise_error(expected_exception = StandardError, message = nil)
|
|
196
|
+
RaiseErrorMatcher.new(expected_exception, message)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def respond_to(*methods)
|
|
200
|
+
RespondToMatcher.new(*methods)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "space"
|
|
4
|
+
|
|
5
|
+
module Crspec
|
|
6
|
+
module Mock
|
|
7
|
+
class MockError < StandardError; end
|
|
8
|
+
|
|
9
|
+
class Double
|
|
10
|
+
attr_reader :name
|
|
11
|
+
|
|
12
|
+
def initialize(name = nil, stubs = {})
|
|
13
|
+
@name = name || "Double"
|
|
14
|
+
Space.current.register_double(self)
|
|
15
|
+
stubs.each do |method_name, return_value|
|
|
16
|
+
Space.current.register_stub(self, method_name, proc { return_value })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def verify_expectations!
|
|
21
|
+
# Verified via Space
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
"#<Double #{@name.inspect}>"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class StubChain
|
|
30
|
+
attr_reader :target, :method_name
|
|
31
|
+
|
|
32
|
+
def initialize(target, method_name)
|
|
33
|
+
@target = target
|
|
34
|
+
@method_name = method_name.to_sym
|
|
35
|
+
@expected_args = nil
|
|
36
|
+
@return_proc = proc {}
|
|
37
|
+
register_default_stub
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with(*args)
|
|
41
|
+
@expected_args = args
|
|
42
|
+
update_stub
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def and_return(*values)
|
|
47
|
+
if values.size == 1
|
|
48
|
+
val = values.first
|
|
49
|
+
@return_proc = proc { val }
|
|
50
|
+
else
|
|
51
|
+
idx = 0
|
|
52
|
+
@return_proc = proc do
|
|
53
|
+
res = values[idx] || values.last
|
|
54
|
+
idx += 1
|
|
55
|
+
res
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
update_stub
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def and_raise(exception = StandardError, message = nil)
|
|
63
|
+
@return_proc = proc do
|
|
64
|
+
err = exception.is_a?(Class) ? exception.new(message) : exception
|
|
65
|
+
raise err
|
|
66
|
+
end
|
|
67
|
+
update_stub
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def and_yield(*yield_args)
|
|
72
|
+
old_proc = @return_proc
|
|
73
|
+
@return_proc = proc do |*args, &block|
|
|
74
|
+
block&.call(*yield_args)
|
|
75
|
+
old_proc.call(*args)
|
|
76
|
+
end
|
|
77
|
+
update_stub
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def register_default_stub
|
|
84
|
+
update_stub
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def update_stub
|
|
88
|
+
target = @target
|
|
89
|
+
method_name = @method_name
|
|
90
|
+
expected_args = @expected_args
|
|
91
|
+
return_proc = @return_proc
|
|
92
|
+
|
|
93
|
+
implementation = proc do |*args, **kwargs, &block|
|
|
94
|
+
if expected_args && args != expected_args
|
|
95
|
+
raise MockError, "Expected #{method_name} with #{expected_args.inspect}, got #{args.inspect}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return_proc.call(*args, **kwargs, &block)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
Space.current.register_stub(target, method_name, implementation)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class ExpectationChain < StubChain
|
|
106
|
+
def initialize(target, method_name)
|
|
107
|
+
@call_count = 0
|
|
108
|
+
@expected_count = 1
|
|
109
|
+
@count_constraint = :exact
|
|
110
|
+
super(target, method_name)
|
|
111
|
+
Space.current.register_expectation(self)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def once
|
|
115
|
+
@expected_count = 1
|
|
116
|
+
@count_constraint = :exact
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def twice
|
|
121
|
+
@expected_count = 2
|
|
122
|
+
@count_constraint = :exact
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def exactly(n)
|
|
127
|
+
@expected_count = n
|
|
128
|
+
@count_constraint = :exact
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def at_least(n)
|
|
133
|
+
@expected_count = n
|
|
134
|
+
@count_constraint = :at_least
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def verify_expectations!
|
|
139
|
+
valid = case @count_constraint
|
|
140
|
+
when :exact then @call_count == @expected_count
|
|
141
|
+
when :at_least then @call_count >= @expected_count
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return if valid
|
|
145
|
+
|
|
146
|
+
raise MockError,
|
|
147
|
+
"Expected #{@target.inspect} to receive #{@method_name.inspect} #{@count_constraint} #{@expected_count} times, but received it #{@call_count} times"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def update_stub
|
|
153
|
+
target = @target
|
|
154
|
+
method_name = @method_name
|
|
155
|
+
expected_args = @expected_args
|
|
156
|
+
return_proc = @return_proc
|
|
157
|
+
|
|
158
|
+
implementation = proc do |*args, **kwargs, &block|
|
|
159
|
+
@call_count += 1
|
|
160
|
+
if expected_args && args != expected_args
|
|
161
|
+
raise MockError, "Expected #{method_name} with #{expected_args.inspect}, got #{args.inspect}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
return_proc.call(*args, **kwargs, &block)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
Space.current.register_stub(target, method_name, implementation)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class ReceiveMatcher
|
|
172
|
+
attr_reader :method_name
|
|
173
|
+
|
|
174
|
+
def initialize(method_name)
|
|
175
|
+
@method_name = method_name.to_sym
|
|
176
|
+
@expected_args = nil
|
|
177
|
+
@return_values = nil
|
|
178
|
+
@raise_exception = nil
|
|
179
|
+
@yield_args = nil
|
|
180
|
+
@expected_count = 1
|
|
181
|
+
@count_constraint = :exact
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def with(*args)
|
|
185
|
+
@expected_args = args
|
|
186
|
+
self
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def and_return(*values)
|
|
190
|
+
@return_values = values
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def and_raise(exception = StandardError, message = nil)
|
|
195
|
+
@raise_exception = exception
|
|
196
|
+
@raise_message = message
|
|
197
|
+
self
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def and_yield(*args)
|
|
201
|
+
@yield_args = args
|
|
202
|
+
self
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def once
|
|
206
|
+
@expected_count = 1
|
|
207
|
+
@count_constraint = :exact
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def twice
|
|
212
|
+
@expected_count = 2
|
|
213
|
+
@count_constraint = :exact
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def exactly(n)
|
|
218
|
+
@expected_count = n
|
|
219
|
+
@count_constraint = :exact
|
|
220
|
+
self
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def at_least(n)
|
|
224
|
+
@expected_count = n
|
|
225
|
+
@count_constraint = :at_least
|
|
226
|
+
self
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def setup_allow(target)
|
|
230
|
+
chain = StubChain.new(target, @method_name)
|
|
231
|
+
apply_chain_options(chain)
|
|
232
|
+
chain
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def setup_expect(target)
|
|
236
|
+
chain = ExpectationChain.new(target, @method_name)
|
|
237
|
+
chain.instance_variable_set(:@expected_count, @expected_count)
|
|
238
|
+
chain.instance_variable_set(:@count_constraint, @count_constraint)
|
|
239
|
+
apply_chain_options(chain)
|
|
240
|
+
chain
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
def apply_chain_options(chain)
|
|
246
|
+
chain.with(*@expected_args) if @expected_args
|
|
247
|
+
chain.and_return(*@return_values) if @return_values
|
|
248
|
+
chain.and_raise(@raise_exception, @raise_message) if @raise_exception
|
|
249
|
+
chain.and_yield(*@yield_args) if @yield_args
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
class AllowTarget
|
|
254
|
+
def initialize(target)
|
|
255
|
+
@target = target
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def to(matcher)
|
|
259
|
+
if matcher.respond_to?(:setup_allow)
|
|
260
|
+
matcher.setup_allow(@target)
|
|
261
|
+
else
|
|
262
|
+
matcher.call(@target)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
module DSL
|
|
268
|
+
def double(name = nil, stubs = {})
|
|
269
|
+
Double.new(name, stubs)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def instance_double(target_class, name = nil, stubs = {})
|
|
273
|
+
Double.new(name || target_class.name, stubs)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def allow(target)
|
|
277
|
+
AllowTarget.new(target)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def receive(method_name)
|
|
281
|
+
ReceiveMatcher.new(method_name)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crspec
|
|
4
|
+
module Mock
|
|
5
|
+
module Interceptor
|
|
6
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
7
|
+
space = Fiber[Space::STORAGE_KEY]
|
|
8
|
+
if space && (stub = space.fetch_stub(self, method_name))
|
|
9
|
+
stub.call(*args, **kwargs, &block)
|
|
10
|
+
else
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
16
|
+
space = Fiber[Space::STORAGE_KEY]
|
|
17
|
+
space&.fetch_stub(self, method_name) || super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.add_intercept_method(method_name)
|
|
21
|
+
method_sym = method_name.to_sym
|
|
22
|
+
return if instance_methods(false).include?(method_sym) || private_instance_methods(false).include?(method_sym)
|
|
23
|
+
|
|
24
|
+
define_method(method_sym) do |*args, **kwargs, &block|
|
|
25
|
+
space = Fiber[Space::STORAGE_KEY]
|
|
26
|
+
if space && (stub = space.fetch_stub(self, method_sym))
|
|
27
|
+
stub.call(*args, **kwargs, &block)
|
|
28
|
+
else
|
|
29
|
+
super(*args, **kwargs, &block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|