sus 0.7.0 → 0.9.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bin/sus +11 -4
- data/bin/sus-parallel +4 -5
- data/lib/sus/assertions.rb +137 -59
- data/lib/sus/base.rb +12 -9
- data/lib/sus/be.rb +8 -4
- data/lib/sus/be_within.rb +2 -2
- data/lib/sus/clock.rb +40 -0
- data/lib/sus/config.rb +133 -5
- data/lib/sus/expect.rb +14 -7
- data/lib/sus/file.rb +5 -3
- data/lib/sus/filter.rb +1 -2
- data/lib/sus/it.rb +2 -2
- data/lib/sus/let.rb +4 -4
- data/lib/sus/loader.rb +11 -0
- data/lib/sus/mock.rb +127 -0
- data/lib/sus/output/backtrace.rb +73 -0
- data/lib/sus/output/buffered.rb +38 -17
- data/lib/sus/output/lines.rb +1 -0
- data/lib/sus/output/null.rb +9 -0
- data/lib/sus/output/progress.rb +146 -0
- data/lib/sus/output/text.rb +11 -0
- data/lib/sus/output.rb +10 -4
- data/lib/sus/raise_exception.rb +5 -10
- data/lib/sus/receive.rb +167 -0
- data/lib/sus/registry.rb +5 -1
- data/lib/sus/respond_to.rb +89 -0
- data/lib/sus/version.rb +1 -1
- data/lib/sus.rb +3 -0
- data.tar.gz.sig +0 -0
- metadata +39 -4
- metadata.gz.sig +0 -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: 34208c06066d1f09d5a32350e26efc277bbd6d322df448bd55cec6070ea15c09
|
4
|
+
data.tar.gz: 9969914ac2241c539244e362e876096c5d0c7dbe95e830db021d6dc46965c5c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fcf93e09b4e3f60a7ff2f01805bf3a11679f219ad382117b023fc575ae2ada0cb0126151d87922d3b5d2a5c384dc0ae42617332142802b580e09aaa68bb54db
|
7
|
+
data.tar.gz: 6c3600065acc4e25a0adf4473fbd0e87ae1901a1238802a4baedf1a45f3c556b6b8d4d46c7b8eb540c87c64d9c82350302c77c669c942fa39c3d713c052d7ac2
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/bin/sus
CHANGED
@@ -4,13 +4,20 @@ require_relative '../lib/sus/config'
|
|
4
4
|
config = Sus::Config.load
|
5
5
|
|
6
6
|
require_relative '../lib/sus'
|
7
|
-
|
8
|
-
assertions = Sus::Assertions.default(output: Sus::Output::Null.new)
|
7
|
+
registry = config.registry
|
9
8
|
|
10
|
-
config.
|
9
|
+
if config.verbose?
|
10
|
+
output = config.output
|
11
|
+
verbose = true
|
12
|
+
else
|
13
|
+
output = Sus::Output::Null.new
|
14
|
+
verbose = false
|
15
|
+
end
|
16
|
+
|
17
|
+
assertions = Sus::Assertions.default(output: output, verbose: verbose)
|
11
18
|
|
12
19
|
config.before_tests(assertions)
|
13
|
-
|
20
|
+
registry.call(assertions)
|
14
21
|
config.after_tests(assertions)
|
15
22
|
|
16
23
|
if assertions.failed.any?
|
data/bin/sus-parallel
CHANGED
@@ -6,20 +6,19 @@ 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
|
17
17
|
|
18
18
|
loader = Thread.new do
|
19
|
-
|
20
|
-
config.prepare(filter)
|
19
|
+
registry = config.registry
|
21
20
|
|
22
|
-
|
21
|
+
registry.each do |child|
|
23
22
|
guard.synchronize{progress.expand}
|
24
23
|
jobs << child
|
25
24
|
end
|
data/lib/sus/assertions.rb
CHANGED
@@ -1,20 +1,34 @@
|
|
1
1
|
|
2
2
|
require_relative 'output'
|
3
|
+
require_relative 'clock'
|
4
|
+
|
5
|
+
require_relative 'output/backtrace'
|
3
6
|
|
4
7
|
module Sus
|
5
8
|
class Assertions
|
6
9
|
def self.default(**options)
|
7
|
-
self.new(**options
|
10
|
+
self.new(**options)
|
8
11
|
end
|
9
12
|
|
10
|
-
def initialize(target: nil, output: Output.
|
13
|
+
def initialize(identity: nil, target: nil, output: Output.buffered, inverted: false, isolated: false, measure: false, verbose: false)
|
14
|
+
# In theory, the target could carry the identity of the assertion group, but it's not really necessary, so we just handle it explicitly and pass it into any nested assertions.
|
15
|
+
@identity = identity
|
11
16
|
@target = target
|
12
17
|
@output = output
|
13
18
|
@inverted = inverted
|
19
|
+
@isolated = isolated
|
14
20
|
@verbose = verbose
|
15
21
|
|
22
|
+
if measure
|
23
|
+
@clock = Clock.new
|
24
|
+
else
|
25
|
+
@clock = nil
|
26
|
+
end
|
27
|
+
|
16
28
|
@passed = Array.new
|
17
29
|
@failed = Array.new
|
30
|
+
@deferred = Array.new
|
31
|
+
|
18
32
|
@count = 0
|
19
33
|
end
|
20
34
|
|
@@ -22,19 +36,25 @@ module Sus
|
|
22
36
|
attr :output
|
23
37
|
attr :level
|
24
38
|
attr :inverted
|
39
|
+
attr :isolated
|
25
40
|
attr :verbose
|
26
41
|
|
27
|
-
|
42
|
+
attr :clock
|
43
|
+
|
44
|
+
# Nested assertions that have passed.
|
28
45
|
attr :passed
|
29
46
|
|
30
|
-
#
|
47
|
+
# Nested assertions that have failed.
|
31
48
|
attr :failed
|
32
49
|
|
50
|
+
# Nested assertions have been deferred.
|
51
|
+
attr :deferred
|
52
|
+
|
33
53
|
# The total number of assertions performed:
|
34
54
|
attr :count
|
35
55
|
|
36
56
|
def inspect
|
37
|
-
"\#<#{self.class} #{@passed.size} passed #{@failed.size} failed>"
|
57
|
+
"\#<#{self.class} #{@passed.size} passed #{@failed.size} failed #{@deferred.size} deferred>"
|
38
58
|
end
|
39
59
|
|
40
60
|
def total
|
@@ -42,8 +62,6 @@ module Sus
|
|
42
62
|
end
|
43
63
|
|
44
64
|
def print(output, verbose: @verbose)
|
45
|
-
self
|
46
|
-
|
47
65
|
if verbose && @target
|
48
66
|
@target.print(output)
|
49
67
|
output.write(": ")
|
@@ -60,6 +78,10 @@ module Sus
|
|
60
78
|
output.write(:failed, @failed.size, " failed", :reset, " ")
|
61
79
|
end
|
62
80
|
|
81
|
+
if @deferred.any?
|
82
|
+
output.write(:deferred, @deferred.size, " deferred", :reset, " ")
|
83
|
+
end
|
84
|
+
|
63
85
|
output.write("out of ", self.total, " total (", @count, " assertions)")
|
64
86
|
end
|
65
87
|
end
|
@@ -68,115 +90,171 @@ module Sus
|
|
68
90
|
@output.puts(:indent, *message)
|
69
91
|
end
|
70
92
|
|
93
|
+
def empty?
|
94
|
+
@passed.empty? and @failed.empty?
|
95
|
+
end
|
96
|
+
|
71
97
|
def passed?
|
72
|
-
@
|
98
|
+
if @inverted
|
99
|
+
# Inverted assertions:
|
100
|
+
self.failed.any?
|
101
|
+
else
|
102
|
+
# Normal assertions:
|
103
|
+
self.failed.empty?
|
104
|
+
end
|
73
105
|
end
|
74
106
|
|
75
107
|
def failed?
|
76
|
-
|
108
|
+
!self.passed?
|
77
109
|
end
|
78
110
|
|
79
111
|
def assert(condition, message = nil)
|
80
112
|
@count += 1
|
81
113
|
|
82
|
-
if @inverted
|
83
|
-
condition = !condition
|
84
|
-
end
|
85
|
-
|
86
114
|
if condition
|
87
115
|
@passed << self
|
88
116
|
|
89
|
-
if @verbose
|
90
|
-
@output.puts(:indent, :passed, pass_prefix, message || "assertion")
|
117
|
+
if @inverted || @verbose
|
118
|
+
@output.puts(:indent, :passed, pass_prefix, message || "assertion", Output::Backtrace.first(@identity))
|
91
119
|
end
|
92
120
|
else
|
93
121
|
@failed << self
|
94
122
|
|
95
|
-
|
123
|
+
if !@inverted || @verbose
|
124
|
+
@output.puts(:indent, :failed, fail_prefix, message || "assertion", Output::Backtrace.first(@identity))
|
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
|
+
@output.indented do
|
142
|
+
while block = @deferred.shift
|
143
|
+
block.call(self)
|
144
|
+
end
|
96
145
|
end
|
97
146
|
end
|
98
147
|
|
99
148
|
def fail(error)
|
100
149
|
@failed << self
|
101
150
|
|
102
|
-
|
103
|
-
|
104
|
-
|
151
|
+
lines = error.message.split(/\r?\n/)
|
152
|
+
|
153
|
+
@output.puts(:indent, :error, fail_prefix, "Unhandled exception ", :value, error.class, ":", :reset, " ", lines.shift)
|
154
|
+
|
155
|
+
lines.each do |line|
|
156
|
+
@output.puts(:indent, "| ", line)
|
105
157
|
end
|
158
|
+
|
159
|
+
@output.write(Output::Backtrace.for(error, @identity))
|
106
160
|
end
|
107
161
|
|
108
|
-
def nested(target, isolated: false, inverted: false, **options)
|
162
|
+
def nested(target, identity: @identity, isolated: false, inverted: false, **options)
|
109
163
|
result = nil
|
110
|
-
output = @output
|
111
|
-
|
112
|
-
if inverted
|
113
|
-
inverted = !@inverted
|
114
|
-
else
|
115
|
-
inverted = @inverted
|
116
|
-
end
|
117
164
|
|
165
|
+
# Isolated assertions need to have buffered output so they can be replayed if they fail:
|
118
166
|
if isolated
|
119
|
-
output =
|
167
|
+
output = @output.buffered
|
168
|
+
else
|
169
|
+
output = @output
|
120
170
|
end
|
121
171
|
|
122
|
-
output.
|
123
|
-
target.print(output)
|
124
|
-
output.puts
|
172
|
+
output.puts(:indent, target)
|
125
173
|
|
126
|
-
assertions = self.class.new(target: target, output: output, inverted: inverted, **options)
|
174
|
+
assertions = self.class.new(identity: identity, target: target, output: output, isolated: isolated, inverted: inverted, verbose: @verbose, **options)
|
127
175
|
|
128
|
-
|
129
|
-
|
176
|
+
@clock&.start!
|
177
|
+
|
178
|
+
output.indented do
|
179
|
+
begin
|
130
180
|
result = yield(assertions)
|
181
|
+
rescue StandardError => error
|
182
|
+
assertions.fail(error)
|
183
|
+
ensure
|
184
|
+
@clock&.stop!
|
131
185
|
end
|
132
|
-
rescue StandardError => error
|
133
|
-
assertions.fail(error)
|
134
186
|
end
|
135
187
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
188
|
+
self.add(assertions)
|
189
|
+
|
190
|
+
return result
|
191
|
+
end
|
192
|
+
|
193
|
+
def add(assertions)
|
194
|
+
# If the assertions should be an isolated group, make sure any deferred assertions are resolved:
|
195
|
+
if assertions.isolated and assertions.deferred?
|
196
|
+
assertions.resolve!
|
197
|
+
end
|
198
|
+
|
199
|
+
if assertions.deferred?
|
200
|
+
self.defer do
|
201
|
+
output.puts(:indent, assertions.target)
|
202
|
+
assertions.resolve!
|
203
|
+
|
204
|
+
self.add!(assertions)
|
141
205
|
end
|
206
|
+
else
|
207
|
+
self.add!(assertions)
|
142
208
|
end
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def add!(assertions)
|
214
|
+
raise "Nested assertions must be fully resolved!" if assertions.deferred?
|
143
215
|
|
144
|
-
|
216
|
+
if assertions.isolated or assertions.inverted
|
217
|
+
# If we are isolated, we merge all child assertions into the parent as a single entity:
|
218
|
+
merge!(assertions)
|
219
|
+
else
|
220
|
+
# Otherwise, we append all child assertions into the parent assertions:
|
221
|
+
append!(assertions)
|
222
|
+
end
|
145
223
|
end
|
146
224
|
|
147
|
-
def merge(assertions)
|
225
|
+
def merge!(assertions)
|
148
226
|
@count += assertions.count
|
149
227
|
|
150
228
|
if assertions.passed?
|
151
229
|
@passed << assertions
|
152
230
|
|
153
|
-
if @verbose
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
end
|
231
|
+
# if @verbose
|
232
|
+
# @output.write(:indent, :passed, pass_prefix, :reset)
|
233
|
+
# self.print(@output, verbose: false)
|
234
|
+
# @output.puts
|
235
|
+
# end
|
158
236
|
else
|
159
237
|
@failed << assertions
|
160
238
|
|
161
|
-
@output.write(:indent, :failed, fail_prefix, :reset)
|
162
|
-
self.print(@output, verbose: false)
|
163
|
-
@output.puts
|
239
|
+
# @output.write(:indent, :failed, fail_prefix, :reset)
|
240
|
+
# self.print(@output, verbose: false)
|
241
|
+
# @output.puts
|
164
242
|
end
|
165
243
|
end
|
166
244
|
|
167
|
-
def
|
245
|
+
def append!(assertions)
|
168
246
|
@count += assertions.count
|
169
247
|
@passed.concat(assertions.passed)
|
170
248
|
@failed.concat(assertions.failed)
|
249
|
+
@deferred.concat(assertions.deferred)
|
171
250
|
|
172
|
-
if @verbose
|
173
|
-
|
174
|
-
|
175
|
-
|
251
|
+
# if @verbose
|
252
|
+
# @output.write(:indent)
|
253
|
+
# self.print(@output, verbose: false)
|
254
|
+
# @output.puts
|
255
|
+
# end
|
176
256
|
end
|
177
|
-
|
178
|
-
private
|
179
|
-
|
257
|
+
|
180
258
|
def pass_prefix
|
181
259
|
"✓ "
|
182
260
|
end
|
data/lib/sus/base.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
|
2
2
|
require_relative 'context'
|
3
|
+
require_relative 'loader'
|
3
4
|
|
4
5
|
module Sus
|
6
|
+
# The base test case class. We need to be careful about what local state is stored.
|
5
7
|
class Base
|
6
8
|
def initialize(assertions)
|
7
|
-
@
|
9
|
+
@__assertions__ = assertions
|
10
|
+
end
|
11
|
+
|
12
|
+
def inspect
|
13
|
+
"\#<Sus::Base for #{self.class.description.inspect}>"
|
8
14
|
end
|
9
15
|
|
10
16
|
def before
|
@@ -22,23 +28,20 @@ module Sus
|
|
22
28
|
end
|
23
29
|
|
24
30
|
def assert(...)
|
25
|
-
@
|
31
|
+
@__assertions__.assert(...)
|
26
32
|
end
|
27
33
|
|
28
34
|
def refute(...)
|
29
|
-
@
|
30
|
-
end
|
31
|
-
|
32
|
-
def expect(subject)
|
33
|
-
Expect.new(subject)
|
35
|
+
@__assertions__.refute(...)
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
def self.base(description =
|
39
|
+
def self.base(description = nil)
|
38
40
|
base = Class.new(Base)
|
41
|
+
|
39
42
|
base.extend(Context)
|
40
43
|
base.description = description
|
41
|
-
|
44
|
+
|
42
45
|
return base
|
43
46
|
end
|
44
47
|
end
|
data/lib/sus/be.rb
CHANGED
@@ -6,13 +6,13 @@ module Sus
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def print(output)
|
9
|
-
|
9
|
+
operation, *arguments = *@arguments
|
10
|
+
|
11
|
+
output.write("be ", :be, operation.to_s, :reset, " ", :variable, arguments.map(&:inspect).join, :reset)
|
10
12
|
end
|
11
13
|
|
12
14
|
def call(assertions, subject)
|
13
|
-
assertions.
|
14
|
-
assertions.assert(subject.public_send(*@arguments), subject)
|
15
|
-
end
|
15
|
+
assertions.assert(subject.public_send(*@arguments), self)
|
16
16
|
end
|
17
17
|
|
18
18
|
class << self
|
@@ -58,5 +58,9 @@ module Sus
|
|
58
58
|
Be
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
def be_a(klass)
|
63
|
+
Be.new(:is_a?, klass)
|
64
|
+
end
|
61
65
|
end
|
62
66
|
end
|
data/lib/sus/be_within.rb
CHANGED
@@ -7,7 +7,7 @@ module Sus
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def print(output)
|
10
|
-
output.write("be within ", :variable, @range)
|
10
|
+
output.write("be within ", :variable, @range, :reset)
|
11
11
|
end
|
12
12
|
|
13
13
|
def call(assertions, subject)
|
@@ -30,7 +30,7 @@ module Sus
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def print(output)
|
33
|
-
output.write("be within ", :variable, @tolerance)
|
33
|
+
output.write("be within ", :variable, @tolerance, :reset)
|
34
34
|
end
|
35
35
|
|
36
36
|
def call(assertions, subject)
|
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,10 @@
|
|
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
|
+
require_relative 'registry'
|
25
|
+
require_relative 'loader'
|
26
|
+
|
23
27
|
module Sus
|
24
28
|
class Config
|
25
29
|
PATH = "config/sus.rb"
|
@@ -32,7 +36,7 @@ module Sus
|
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
35
|
-
def self.load(root: Dir.pwd,
|
39
|
+
def self.load(root: Dir.pwd, arguments: ARGV)
|
36
40
|
derived = Class.new(self)
|
37
41
|
|
38
42
|
if path = self.path(root)
|
@@ -41,12 +45,30 @@ module Sus
|
|
41
45
|
derived.prepend(config)
|
42
46
|
end
|
43
47
|
|
44
|
-
|
48
|
+
options = {
|
49
|
+
verbose: !!arguments.delete('--verbose')
|
50
|
+
}
|
51
|
+
|
52
|
+
return derived.new(root, arguments, **options)
|
45
53
|
end
|
46
54
|
|
47
|
-
def initialize(root, paths)
|
55
|
+
def initialize(root, paths, verbose: false)
|
48
56
|
@root = root
|
49
57
|
@paths = paths
|
58
|
+
@verbose = verbose
|
59
|
+
|
60
|
+
@clock = Clock.new
|
61
|
+
end
|
62
|
+
|
63
|
+
attr :root
|
64
|
+
attr :paths
|
65
|
+
|
66
|
+
def verbose?
|
67
|
+
@verbose
|
68
|
+
end
|
69
|
+
|
70
|
+
def partial?
|
71
|
+
@paths.any?
|
50
72
|
end
|
51
73
|
|
52
74
|
def output
|
@@ -59,8 +81,26 @@ module Sus
|
|
59
81
|
return Dir.glob(DEFAULT_TEST_PATTERN, base: @root)
|
60
82
|
end
|
61
83
|
|
62
|
-
def
|
84
|
+
def registry
|
85
|
+
@registry ||= self.load_registry
|
86
|
+
end
|
87
|
+
|
88
|
+
def base
|
89
|
+
Sus.base
|
90
|
+
end
|
91
|
+
|
92
|
+
def setup_base(base)
|
93
|
+
base.extend(Loader)
|
94
|
+
base.define_singleton_method(:require_root) {self.root}
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_registry
|
98
|
+
registry = Sus::Registry.new
|
99
|
+
|
100
|
+
self.setup_base(registry.base)
|
101
|
+
|
63
102
|
if @paths&.any?
|
103
|
+
registry = Sus::Filter.new(registry)
|
64
104
|
@paths.each do |path|
|
65
105
|
registry.load(path)
|
66
106
|
end
|
@@ -69,22 +109,110 @@ module Sus
|
|
69
109
|
registry.load(path)
|
70
110
|
end
|
71
111
|
end
|
112
|
+
|
113
|
+
return registry
|
72
114
|
end
|
73
115
|
|
74
116
|
def before_tests(assertions)
|
117
|
+
@clock.start!
|
75
118
|
end
|
76
119
|
|
77
120
|
def after_tests(assertions)
|
121
|
+
@clock.stop!
|
122
|
+
|
123
|
+
self.print_summary(assertions)
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def print_summary(assertions)
|
78
129
|
output = self.output
|
79
130
|
|
80
131
|
assertions.print(output)
|
81
132
|
output.puts
|
133
|
+
|
134
|
+
print_finished_statistics(assertions)
|
135
|
+
|
136
|
+
if !partial? and assertions.passed?
|
137
|
+
print_test_feedback(assertions)
|
138
|
+
end
|
139
|
+
|
140
|
+
print_slow_tests(assertions)
|
141
|
+
print_failed_assertions(assertions)
|
142
|
+
end
|
143
|
+
|
144
|
+
def print_finished_statistics(assertions)
|
145
|
+
duration = @clock.duration
|
146
|
+
rate = assertions.count / duration
|
147
|
+
|
148
|
+
output.puts "🏁 Finished in ", @clock, "; #{rate.round(3)} assertions per second."
|
149
|
+
end
|
150
|
+
|
151
|
+
def print_test_feedback(assertions)
|
152
|
+
duration = @clock.duration
|
153
|
+
rate = assertions.count / duration
|
154
|
+
|
155
|
+
total = assertions.total
|
156
|
+
count = assertions.count
|
157
|
+
|
158
|
+
if total < 10 or count < 10
|
159
|
+
output.puts "😭 You should write more tests and assertions!"
|
160
|
+
|
161
|
+
# Statistics will be less meaningful with such a small amount of data, so give up:
|
162
|
+
return
|
163
|
+
end
|
164
|
+
|
165
|
+
# Check whether there is at least, on average, one assertion (or more) per test:
|
166
|
+
assertions_per_test = assertions.count / assertions.total
|
167
|
+
if assertions_per_test < 1.0
|
168
|
+
output.puts "😩 Your tests don't have enough assertions (#{assertions_per_test.round(1)} < 1.0)!"
|
169
|
+
end
|
170
|
+
|
171
|
+
# Give some feedback about the number of tests:
|
172
|
+
if total < 20
|
173
|
+
output.puts "🥲 You should write more tests (#{total}/20)!"
|
174
|
+
elsif total < 50
|
175
|
+
output.puts "🙂 Your test suite is starting to shape up, keep on at it (#{total}/50)!"
|
176
|
+
elsif total < 100
|
177
|
+
output.puts "😀 Your test suite is maturing, keep on at it (#{total}/100)!"
|
178
|
+
else
|
179
|
+
output.puts "🤩 Your test suite is amazing!"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Give some feedback about the performance of the tests:
|
183
|
+
if rate < 10.0
|
184
|
+
output.puts "💔 Ouch! Your test suite performance is painful (#{rate.round(1)} < 10)!"
|
185
|
+
elsif rate < 100.0
|
186
|
+
output.puts "💩 Oops! Your test suite performance could be better (#{rate.round(1)} < 100)!"
|
187
|
+
elsif rate < 1_000.0
|
188
|
+
output.puts "💪 Good job! Your test suite has good performance (#{rate.round(1)} < 1000)!"
|
189
|
+
elsif rate < 10_000.0
|
190
|
+
output.puts "🎉 Great job! Your test suite has excellent performance (#{rate.round(1)} < 10000)!"
|
191
|
+
else
|
192
|
+
output.puts "🔥 Wow! Your test suite has outstanding performance (#{rate.round(1)} >= 10000.0)!"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def print_slow_tests(assertions, threshold = 0.1)
|
197
|
+
slowest_tests = assertions.passed.select{|test| test.clock > threshold}.sort_by(&:clock).reverse!
|
82
198
|
|
199
|
+
if slowest_tests.empty?
|
200
|
+
output.puts "🐇 No slow tests found! Well done!"
|
201
|
+
else
|
202
|
+
output.puts "🐢 Slow tests:"
|
203
|
+
|
204
|
+
slowest_tests.each do |test|
|
205
|
+
output.puts "\t", :variable, test.clock, :reset, ": ", test.target
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def print_failed_assertions(assertions)
|
83
211
|
if assertions.failed.any?
|
84
212
|
output.puts
|
85
213
|
|
86
214
|
assertions.failed.each do |failure|
|
87
|
-
|
215
|
+
output.append(failure.output)
|
88
216
|
end
|
89
217
|
end
|
90
218
|
end
|