sus 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bin/sus-parallel +2 -2
- data/lib/sus/assertions.rb +103 -38
- data/lib/sus/base.rb +0 -4
- data/lib/sus/be.rb +1 -3
- data/lib/sus/clock.rb +40 -0
- data/lib/sus/config.rb +90 -1
- data/lib/sus/expect.rb +14 -7
- data/lib/sus/it.rb +1 -1
- data/lib/sus/mock.rb +127 -0
- data/lib/sus/output/buffered.rb +13 -17
- data/lib/sus/output/lines.rb +1 -0
- data/lib/sus/output/null.rb +3 -0
- data/lib/sus/output/progress.rb +146 -0
- data/lib/sus/output/text.rb +6 -0
- data/lib/sus/output.rb +2 -1
- data/lib/sus/raise_exception.rb +5 -10
- data/lib/sus/receive.rb +147 -0
- data/lib/sus/respond_to.rb +86 -0
- data/lib/sus/version.rb +1 -1
- data/lib/sus.rb +3 -0
- data.tar.gz.sig +0 -0
- metadata +37 -4
- metadata.gz.sig +2 -0
- data/lib/sus/progress.rb +0 -144
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37d794368517fecc66824b3fffff17d0849df9cdc100df5cf8ecb40bce1c2b1d
|
4
|
+
data.tar.gz: b8395a5ce04c2fe773f6135e6a930a5b05d5c0efa6e566d5d02e087862ea7ed7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6407424e83d8a8068715c04472e5de5fec9a8570601cc9fc4b340c12236e98b85a9d3ef7bafddb3ccd567c6136d23175fd80ecc79b83fe39ba1990d1da593eee
|
7
|
+
data.tar.gz: 112c5ccafab10d46cef22ad1ebd11fbc9ed343bfae3df8d121ea0cf72c9126576d78e3bb524f770aa8d9669db75309a4e221b6265c52ca7de4fd34e1c27047dd
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/bin/sus-parallel
CHANGED
@@ -6,11 +6,11 @@ config = Sus::Config.load
|
|
6
6
|
Result = Struct.new(:job, :assertions)
|
7
7
|
|
8
8
|
require_relative '../lib/sus'
|
9
|
-
require_relative '../lib/sus/
|
9
|
+
require_relative '../lib/sus/output'
|
10
10
|
jobs = Thread::Queue.new
|
11
11
|
results = Thread::Queue.new
|
12
12
|
guard = Thread::Mutex.new
|
13
|
-
progress = Sus::Progress.new(config.output)
|
13
|
+
progress = Sus::Output::Progress.new(config.output)
|
14
14
|
|
15
15
|
require 'etc'
|
16
16
|
count = Etc.nprocessors
|
data/lib/sus/assertions.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
require_relative 'output'
|
3
|
+
require_relative 'clock'
|
3
4
|
|
4
5
|
module Sus
|
5
6
|
class Assertions
|
@@ -7,14 +8,23 @@ module Sus
|
|
7
8
|
self.new(**options, verbose: true)
|
8
9
|
end
|
9
10
|
|
10
|
-
def initialize(target: nil, output: Output.
|
11
|
+
def initialize(target: nil, output: Output.buffered, inverted: false, isolated: false, measure: false, verbose: false)
|
11
12
|
@target = target
|
12
13
|
@output = output
|
13
14
|
@inverted = inverted
|
15
|
+
@isolated = isolated
|
14
16
|
@verbose = verbose
|
15
17
|
|
18
|
+
if measure
|
19
|
+
@clock = Clock.new
|
20
|
+
else
|
21
|
+
@clock = nil
|
22
|
+
end
|
23
|
+
|
16
24
|
@passed = Array.new
|
17
25
|
@failed = Array.new
|
26
|
+
@deferred = Array.new
|
27
|
+
|
18
28
|
@count = 0
|
19
29
|
end
|
20
30
|
|
@@ -22,19 +32,25 @@ module Sus
|
|
22
32
|
attr :output
|
23
33
|
attr :level
|
24
34
|
attr :inverted
|
35
|
+
attr :isolated
|
25
36
|
attr :verbose
|
26
37
|
|
27
|
-
|
38
|
+
attr :clock
|
39
|
+
|
40
|
+
# Nested assertions that have passed.
|
28
41
|
attr :passed
|
29
42
|
|
30
|
-
#
|
43
|
+
# Nested assertions that have failed.
|
31
44
|
attr :failed
|
32
45
|
|
46
|
+
# Nested assertions have been deferred.
|
47
|
+
attr :deferred
|
48
|
+
|
33
49
|
# The total number of assertions performed:
|
34
50
|
attr :count
|
35
51
|
|
36
52
|
def inspect
|
37
|
-
"\#<#{self.class} #{@passed.size} passed #{@failed.size} failed>"
|
53
|
+
"\#<#{self.class} #{self.object_id} #{@passed.size} passed #{@failed.size} failed #{@deferred.size} deferred>"
|
38
54
|
end
|
39
55
|
|
40
56
|
def total
|
@@ -60,6 +76,10 @@ module Sus
|
|
60
76
|
output.write(:failed, @failed.size, " failed", :reset, " ")
|
61
77
|
end
|
62
78
|
|
79
|
+
if @deferred.any?
|
80
|
+
output.write(:deferred, @deferred.size, " deferred", :reset, " ")
|
81
|
+
end
|
82
|
+
|
63
83
|
output.write("out of ", self.total, " total (", @count, " assertions)")
|
64
84
|
end
|
65
85
|
end
|
@@ -68,31 +88,58 @@ module Sus
|
|
68
88
|
@output.puts(:indent, *message)
|
69
89
|
end
|
70
90
|
|
91
|
+
def empty?
|
92
|
+
@passed.empty? and @failed.empty?
|
93
|
+
end
|
94
|
+
|
71
95
|
def passed?
|
72
|
-
@
|
96
|
+
if @inverted
|
97
|
+
# Inverted assertions:
|
98
|
+
self.failed.any?
|
99
|
+
else
|
100
|
+
# Normal assertions:
|
101
|
+
self.failed.empty?
|
102
|
+
end
|
73
103
|
end
|
74
104
|
|
75
105
|
def failed?
|
76
|
-
|
106
|
+
!self.passed?
|
77
107
|
end
|
78
108
|
|
79
109
|
def assert(condition, message = nil)
|
80
|
-
|
110
|
+
# sleep 0.5
|
81
111
|
|
82
|
-
|
83
|
-
condition = !condition
|
84
|
-
end
|
112
|
+
@count += 1
|
85
113
|
|
86
114
|
if condition
|
87
115
|
@passed << self
|
88
116
|
|
89
|
-
|
117
|
+
@output.indented do
|
90
118
|
@output.puts(:indent, :passed, pass_prefix, message || "assertion")
|
91
119
|
end
|
92
120
|
else
|
93
121
|
@failed << self
|
94
122
|
|
95
|
-
@output.
|
123
|
+
@output.indented do
|
124
|
+
@output.puts(:indent, :failed, fail_prefix, message || "assertion")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add deferred assertions.
|
130
|
+
def defer(&block)
|
131
|
+
@deferred << block
|
132
|
+
end
|
133
|
+
|
134
|
+
# Whether there are any deferred assertions.
|
135
|
+
def deferred?
|
136
|
+
@deferred.any?
|
137
|
+
end
|
138
|
+
|
139
|
+
# This resolves all deferred assertions in order.
|
140
|
+
def resolve!
|
141
|
+
while block = @deferred.shift
|
142
|
+
block.call(self)
|
96
143
|
end
|
97
144
|
end
|
98
145
|
|
@@ -107,44 +154,63 @@ module Sus
|
|
107
154
|
|
108
155
|
def nested(target, isolated: false, inverted: false, **options)
|
109
156
|
result = nil
|
110
|
-
output =
|
111
|
-
|
112
|
-
if inverted
|
113
|
-
inverted = !@inverted
|
114
|
-
else
|
115
|
-
inverted = @inverted
|
116
|
-
end
|
117
|
-
|
118
|
-
if isolated
|
119
|
-
output = Output::Buffered.new(output)
|
120
|
-
end
|
157
|
+
output = Output::Buffered.new
|
121
158
|
|
122
159
|
output.write(:indent)
|
123
160
|
target.print(output)
|
124
161
|
output.puts
|
125
162
|
|
126
|
-
assertions = self.class.new(target: target, output: output, inverted: inverted, **options)
|
163
|
+
assertions = self.class.new(target: target, output: output, isolated: isolated, inverted: inverted, **options)
|
127
164
|
|
128
165
|
begin
|
129
|
-
|
130
|
-
|
131
|
-
end
|
166
|
+
@clock&.start!
|
167
|
+
result = yield(assertions)
|
132
168
|
rescue StandardError => error
|
133
169
|
assertions.fail(error)
|
170
|
+
ensure
|
171
|
+
@clock&.stop!
|
172
|
+
end
|
173
|
+
|
174
|
+
self.add(assertions)
|
175
|
+
|
176
|
+
return result
|
177
|
+
end
|
178
|
+
|
179
|
+
def add(assertions)
|
180
|
+
# If the assertions should be an isolated group, make sure any deferred assertions are resolved:
|
181
|
+
if assertions.isolated
|
182
|
+
assertions.resolve!
|
134
183
|
end
|
135
184
|
|
136
|
-
if assertions
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
add(assertions)
|
185
|
+
if assertions.deferred?
|
186
|
+
self.defer do
|
187
|
+
assertions.resolve!
|
188
|
+
self.add!(assertions)
|
141
189
|
end
|
190
|
+
else
|
191
|
+
self.add!(assertions)
|
142
192
|
end
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def add!(assertions)
|
198
|
+
raise "Nested assertions must be fully resolved!" if assertions.deferred?
|
143
199
|
|
144
|
-
|
200
|
+
if assertions.isolated or assertions.inverted
|
201
|
+
# If we are isolated, we merge all child assertions into the parent as a single entity:
|
202
|
+
merge!(assertions)
|
203
|
+
else
|
204
|
+
# Otherwise, we append all child assertions into the parent assertions:
|
205
|
+
append!(assertions)
|
206
|
+
end
|
207
|
+
|
208
|
+
@output.indented do
|
209
|
+
@output.append(assertions.output)
|
210
|
+
end
|
145
211
|
end
|
146
212
|
|
147
|
-
def merge(assertions)
|
213
|
+
def merge!(assertions)
|
148
214
|
@count += assertions.count
|
149
215
|
|
150
216
|
if assertions.passed?
|
@@ -164,19 +230,18 @@ module Sus
|
|
164
230
|
end
|
165
231
|
end
|
166
232
|
|
167
|
-
def
|
233
|
+
def append!(assertions)
|
168
234
|
@count += assertions.count
|
169
235
|
@passed.concat(assertions.passed)
|
170
236
|
@failed.concat(assertions.failed)
|
237
|
+
@deferred.concat(assertions.deferred)
|
171
238
|
|
172
239
|
if @verbose
|
173
240
|
self.print(@output, verbose: false)
|
174
241
|
@output.puts
|
175
242
|
end
|
176
243
|
end
|
177
|
-
|
178
|
-
private
|
179
|
-
|
244
|
+
|
180
245
|
def pass_prefix
|
181
246
|
"✓ "
|
182
247
|
end
|
data/lib/sus/base.rb
CHANGED
data/lib/sus/be.rb
CHANGED
data/lib/sus/clock.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Sus
|
2
|
+
class Clock
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
def initialize(duration = 0.0)
|
6
|
+
@duration = duration
|
7
|
+
end
|
8
|
+
|
9
|
+
attr :duration
|
10
|
+
|
11
|
+
def <=>(other)
|
12
|
+
@duration <=> other.to_f
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_f
|
16
|
+
@duration
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
if @duration < 0.001
|
21
|
+
"#{(@duration * 1_000_000).round(1)}µs"
|
22
|
+
elsif @duration < 1.0
|
23
|
+
"#{(@duration * 1_000).round(1)}ms"
|
24
|
+
else
|
25
|
+
"#{@duration.round(1)}s"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def start!
|
30
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop!
|
34
|
+
if @start_time
|
35
|
+
@duration += Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
36
|
+
@start_time = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/sus/config.rb
CHANGED
@@ -20,6 +20,8 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
21
|
# THE SOFTWARE.
|
22
22
|
|
23
|
+
require_relative 'clock'
|
24
|
+
|
23
25
|
module Sus
|
24
26
|
class Config
|
25
27
|
PATH = "config/sus.rb"
|
@@ -47,6 +49,7 @@ module Sus
|
|
47
49
|
def initialize(root, paths)
|
48
50
|
@root = root
|
49
51
|
@paths = paths
|
52
|
+
@clock = Clock.new
|
50
53
|
end
|
51
54
|
|
52
55
|
def output
|
@@ -72,19 +75,105 @@ module Sus
|
|
72
75
|
end
|
73
76
|
|
74
77
|
def before_tests(assertions)
|
78
|
+
@clock.start!
|
75
79
|
end
|
76
80
|
|
77
81
|
def after_tests(assertions)
|
82
|
+
@clock.stop!
|
83
|
+
|
78
84
|
output = self.output
|
79
85
|
|
80
86
|
assertions.print(output)
|
81
87
|
output.puts
|
88
|
+
|
89
|
+
print_finished_statistics(assertions)
|
90
|
+
|
91
|
+
if !partial? and assertions.passed?
|
92
|
+
print_test_feedback(assertions)
|
93
|
+
end
|
94
|
+
|
95
|
+
print_slow_tests(assertions)
|
96
|
+
print_failed_assertions(assertions)
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def partial?
|
102
|
+
@paths.any?
|
103
|
+
end
|
104
|
+
|
105
|
+
def print_finished_statistics(assertions)
|
106
|
+
duration = @clock.duration
|
107
|
+
rate = assertions.count / duration
|
108
|
+
|
109
|
+
output.puts "🏁 Finished in ", @clock, "; #{rate.round(3)} assertions per second."
|
110
|
+
end
|
111
|
+
|
112
|
+
def print_test_feedback(assertions)
|
113
|
+
duration = @clock.duration
|
114
|
+
rate = assertions.count / duration
|
115
|
+
|
116
|
+
total = assertions.total
|
117
|
+
count = assertions.count
|
118
|
+
|
119
|
+
if total < 10 or count < 10
|
120
|
+
output.puts "😭 You should write more tests and assertions!"
|
121
|
+
|
122
|
+
# Statistics will be less meaningful with such a small amount of data, so give up:
|
123
|
+
return
|
124
|
+
end
|
82
125
|
|
126
|
+
# Check whether there is at least, on average, one assertion (or more) per test:
|
127
|
+
assertions_per_test = assertions.count / assertions.total
|
128
|
+
if assertions_per_test < 1.0
|
129
|
+
output.puts "😩 Your tests don't have enough assertions (#{assertions_per_test.round(1)} < 1.0)!"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Give some feedback about the number of tests:
|
133
|
+
if total < 20
|
134
|
+
output.puts "🥲 You should write more tests (#{total}/20)!"
|
135
|
+
elsif total < 50
|
136
|
+
output.puts "🙂 Your test suite is starting to shape up, keep on at it (#{total}/50)!"
|
137
|
+
elsif total < 100
|
138
|
+
output.puts "😀 Your test suite is maturing, keep on at it (#{total}/100)!"
|
139
|
+
else
|
140
|
+
output.puts "🤩 Your test suite is amazing!"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Give some feedback about the performance of the tests:
|
144
|
+
if rate < 10.0
|
145
|
+
output.puts "💔 Ouch! Your test suite performance is painful (#{rate.round(1)} < 10)!"
|
146
|
+
elsif rate < 100.0
|
147
|
+
output.puts "💩 Oops! Your test suite performance could be better (#{rate.round(1)} < 100)!"
|
148
|
+
elsif rate < 1_000.0
|
149
|
+
output.puts "💪 Good job! Your test suite has good performance (#{rate.round(1)} < 1000)!"
|
150
|
+
elsif rate < 10_000.0
|
151
|
+
output.puts "🎉 Great job! Your test suite has excellent performance (#{rate.round(1)} < 10000)!"
|
152
|
+
else
|
153
|
+
output.puts "🔥 Wow! Your test suite has outstanding performance (#{rate.round(1)} >= 10000.0)!"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def print_slow_tests(assertions, threshold = 0.1)
|
158
|
+
slowest_tests = assertions.passed.select{|test| test.clock > threshold}.sort_by(&:clock).reverse!
|
159
|
+
|
160
|
+
if slowest_tests.empty?
|
161
|
+
output.puts "🐇 No slow tests found! Well done!"
|
162
|
+
else
|
163
|
+
output.puts "🐢 Slow tests:"
|
164
|
+
|
165
|
+
slowest_tests.each do |test|
|
166
|
+
output.puts "\t", :variable, test.clock, :reset, ": ", test.target
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def print_failed_assertions(assertions)
|
83
172
|
if assertions.failed.any?
|
84
173
|
output.puts
|
85
174
|
|
86
175
|
assertions.failed.each do |failure|
|
87
|
-
|
176
|
+
output.append(failure.output)
|
88
177
|
end
|
89
178
|
end
|
90
179
|
end
|
data/lib/sus/expect.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
|
2
2
|
module Sus
|
3
3
|
class Expect
|
4
|
-
def initialize(assertions, subject)
|
4
|
+
def initialize(assertions, subject, inverted: false)
|
5
5
|
@assertions = assertions
|
6
6
|
@subject = subject
|
7
|
-
@inverted =
|
7
|
+
@inverted = inverted
|
8
8
|
end
|
9
|
+
|
10
|
+
attr :subject
|
11
|
+
attr :inverted
|
9
12
|
|
10
13
|
def not
|
11
14
|
self.dup.tap do |expect|
|
@@ -17,9 +20,9 @@ module Sus
|
|
17
20
|
output.write("expect ", :variable, @subject.inspect, :reset, " ")
|
18
21
|
|
19
22
|
if @inverted
|
20
|
-
output.write(
|
23
|
+
output.write("to not", :reset)
|
21
24
|
else
|
22
|
-
output.write(
|
25
|
+
output.write("to", :reset)
|
23
26
|
end
|
24
27
|
end
|
25
28
|
|
@@ -30,14 +33,18 @@ module Sus
|
|
30
33
|
|
31
34
|
return self
|
32
35
|
end
|
36
|
+
|
37
|
+
def and(predicate)
|
38
|
+
return to(predicate)
|
39
|
+
end
|
33
40
|
end
|
34
41
|
|
35
42
|
class Base
|
36
|
-
def expect(subject = nil, &block)
|
43
|
+
def expect(subject = nil, **options, &block)
|
37
44
|
if block_given?
|
38
|
-
Expect.new(@assertions, block)
|
45
|
+
Expect.new(@assertions, block, **options)
|
39
46
|
else
|
40
|
-
Expect.new(@assertions, subject)
|
47
|
+
Expect.new(@assertions, subject, **options)
|
41
48
|
end
|
42
49
|
end
|
43
50
|
end
|
data/lib/sus/it.rb
CHANGED
data/lib/sus/mock.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright, 2022, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
require_relative 'expect'
|
24
|
+
|
25
|
+
module Sus
|
26
|
+
class Mock
|
27
|
+
def initialize(target)
|
28
|
+
@target = target
|
29
|
+
@interceptor = Module.new
|
30
|
+
|
31
|
+
@target.singleton_class.prepend(@interceptor)
|
32
|
+
end
|
33
|
+
|
34
|
+
attr :target
|
35
|
+
|
36
|
+
def print(output)
|
37
|
+
output.write("mock ", :context, @target.inspect)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@interceptor.instance_methods.each do |method_name|
|
42
|
+
@interceptor.remove_method(method_name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace(method, &hook)
|
47
|
+
execution_context = Thread.current
|
48
|
+
|
49
|
+
@interceptor.define_method(method) do |*arguments, **options, &block|
|
50
|
+
if execution_context == Thread.current
|
51
|
+
hook.call(*arguments, **options, &block)
|
52
|
+
else
|
53
|
+
super(*arguments, **options, &block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
return self
|
58
|
+
end
|
59
|
+
|
60
|
+
def before(method, &hook)
|
61
|
+
execution_context = Thread.current
|
62
|
+
|
63
|
+
@interceptor.define_method(method) do |*arguments, **options, &block|
|
64
|
+
hook.call(*arguments, **options, &block) if execution_context == Thread.current
|
65
|
+
super(*arguments, **options, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
return self
|
69
|
+
end
|
70
|
+
|
71
|
+
def after(method, &hook)
|
72
|
+
execution_context = Thread.current
|
73
|
+
|
74
|
+
@interceptor.define_method(method) do |*arguments, **options, &block|
|
75
|
+
result = super(*arguments, **options, &block)
|
76
|
+
hook.call(result, *arguments, **options, &block) if execution_context == Thread.current
|
77
|
+
return result
|
78
|
+
end
|
79
|
+
|
80
|
+
return self
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module Mocks
|
85
|
+
def after
|
86
|
+
super
|
87
|
+
|
88
|
+
@mocks&.each_value(&:clear)
|
89
|
+
end
|
90
|
+
|
91
|
+
def mock(target)
|
92
|
+
validate_mock!(target)
|
93
|
+
|
94
|
+
mock = self.mocks[target]
|
95
|
+
|
96
|
+
if block_given?
|
97
|
+
yield mock
|
98
|
+
end
|
99
|
+
|
100
|
+
return mock
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
MockTargetError = Class.new(StandardError)
|
106
|
+
|
107
|
+
def validate_mock!(target)
|
108
|
+
if target.frozen?
|
109
|
+
raise MockTargetError, "Cannot mock frozen object #{target.inspect}!"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def mocks
|
114
|
+
@mocks ||= Hash.new{|h,k| h[k] = Mock.new(k)}.compare_by_identity
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Base
|
119
|
+
def mock(target, &block)
|
120
|
+
# Pull in the extra functionality:
|
121
|
+
self.singleton_class.prepend(Mocks)
|
122
|
+
|
123
|
+
# Redirect the method to the new functionality:
|
124
|
+
self.mock(target, &block)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|