sus 0.20.2 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abff2f6428b5b2a3b86b3ad54d32d73f9b78bf58cd1f81bf010cc7593e1c4bc2
4
- data.tar.gz: 4fb6c173ac38b1c2e2053867d7f8ec768a9cfdd90ddfa8db366ef4af799a3211
3
+ metadata.gz: c8d9e361b911f82db22849e86d61769d01ed115b1137639f1dd323c1a7d9f7e8
4
+ data.tar.gz: c24ffc6bc111a1860b5bf01c97510a37d79a4efee8ca93c0f5718f40b23c3e25
5
5
  SHA512:
6
- metadata.gz: 23f742731558ca0bb9b3a2a09bc3fb01a6f5d8fb2af4309c41d750866f3e3a9f25be059f1c186670132b1eeafb7896d40262cec04c20d0f8c41e2b73b9255136
7
- data.tar.gz: e1e4ba8a789c2ca7c1c8391e975499185a0860afc738c3fc0b94dc380c3d22434daef20a0992a07179740f5568afb88a0dda4ef97c1485e96af56ac67d2cce57
6
+ metadata.gz: 2341975dbac7bbb46ed3758f2845158da81310cd9f518cf008c5a04f43da7ca397500db3e24316e3c4ae6918993d0580cb8df0c76c80245fc31b9c2e2a0f4b49
7
+ data.tar.gz: 04f25d46435a3d3eeb303926f8931857b6b4fc19788f393863de1b610d8eadc04ed96b9868502265c4dce74cac5ce4d22677845834d54fd90f6acd0c121e1946
checksums.yaml.gz.sig CHANGED
Binary file
data/bin/sus-host CHANGED
@@ -15,12 +15,14 @@ count = Etc.nprocessors
15
15
 
16
16
  $stdout.sync = true
17
17
 
18
+ require_relative '../lib/sus/output/structured'
19
+
18
20
  input = $stdin.dup
19
21
  $stdin.reopen(File::NULL)
20
22
  output = $stdout.dup
21
23
  $stdout.reopen($stderr)
22
24
 
23
- def failure_messages_for(assertions)
25
+ def messages_for(assertions)
24
26
  messages = []
25
27
 
26
28
  assertions.each_failure do |failure|
@@ -63,17 +65,19 @@ while line = input.gets
63
65
  output.puts JSON.generate({started: job.identity})
64
66
  end
65
67
 
66
- assertions = Sus::Assertions.new(measure: true)
68
+ structured_output = Sus::Output::Structured.buffered(output, job.identity)
69
+
70
+ assertions = Sus::Assertions.new(output: structured_output, measure: true)
67
71
  job.call(assertions)
68
72
  results.push(assertions)
69
73
 
70
74
  guard.synchronize do
71
75
  if assertions.passed?
72
- output.puts JSON.generate({passed: job.identity, duration: assertions.clock.ms})
76
+ output.puts JSON.generate({passed: job.identity, messages: messages_for(assertions), duration: assertions.clock.ms})
73
77
  elsif assertions.errored?
74
- output.puts JSON.generate({errored: job.identity, messages: failure_messages_for(assertions), duration: assertions.clock.ms})
78
+ output.puts JSON.generate({errored: job.identity, messages: messages_for(assertions), duration: assertions.clock.ms})
75
79
  else
76
- output.puts JSON.generate({failed: job.identity, messages: failure_messages_for(assertions), duration: assertions.clock.ms})
80
+ output.puts JSON.generate({failed: job.identity, messages: messages_for(assertions), duration: assertions.clock.ms})
77
81
  end
78
82
  end
79
83
  end
@@ -16,7 +16,7 @@ module Sus
16
16
 
17
17
  # @parameter orientation [Boolean] Whether the assertions are positive or negative in general.
18
18
  # @parameter inverted [Boolean] Whether the assertions are inverted with respect to the parent.
19
- def initialize(identity: nil, target: nil, output: Output.buffered, inverted: false, orientation: true, isolated: false, measure: false, verbose: false)
19
+ def initialize(identity: nil, target: nil, output: Output.buffered, inverted: false, orientation: true, isolated: false, distinct: false, measure: false, verbose: false)
20
20
  # 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.
21
21
  @identity = identity
22
22
  @target = target
@@ -24,6 +24,7 @@ module Sus
24
24
  @inverted = inverted
25
25
  @orientation = orientation
26
26
  @isolated = isolated
27
+ @distinct = distinct
27
28
  @verbose = verbose
28
29
 
29
30
  if measure
@@ -46,13 +47,18 @@ module Sus
46
47
  attr :output
47
48
  attr :level
48
49
 
49
- # Whether this aset of assertions is inverted
50
+ # Whether this aset of assertions is inverted, i.e. the assertions are expected to fail relative to the parent. Used for grouping assertions and ensuring they are added to the parent passed/failed array correctly.
50
51
  attr :inverted
51
52
 
52
- # The absolute orientation of this set of assertions:
53
+ # The absolute orientation of this set of assertions, i.e. whether the assertions are expected to pass or fail regardless of the parent. Used for correctly formatting the output.
53
54
  attr :orientation
54
55
 
56
+ # Whether this set of assertions is isolated from the parent. This is used to ensure taht any deferred assertions are competed before the parent is completed. This is used by `receive` assertions which are deferred until the user code of the test has completed.
55
57
  attr :isolated
58
+
59
+ # Distinct is used to identify a set of assertions as a single statement for the purpose of user feedback. It's used by top level ensure statements to ensure that error messages are captured and reported on those statements.
60
+ attr :distinct
61
+
56
62
  attr :verbose
57
63
 
58
64
  attr :clock
@@ -147,12 +153,14 @@ module Sus
147
153
  end
148
154
 
149
155
  class Assert
150
- def initialize(identity, assertions)
156
+ def initialize(identity, backtrace, assertions)
151
157
  @identity = identity
158
+ @backtrace = backtrace
152
159
  @assertions = assertions
153
160
  end
154
161
 
155
162
  attr :identity
163
+ attr :backtrace
156
164
  attr :assertions
157
165
 
158
166
  def each_failure(&block)
@@ -161,7 +169,8 @@ module Sus
161
169
 
162
170
  def message
163
171
  {
164
- text: assertions.output.string,
172
+ # It's possible that several Assert instances might share the same output text. This is because the output is buffered for each test and each top-level test expectation.
173
+ text: @assertions.output.string,
165
174
  location: @identity&.to_location
166
175
  }
167
176
  end
@@ -170,30 +179,38 @@ module Sus
170
179
  def assert(condition, message = nil)
171
180
  @count += 1
172
181
 
173
- backtrace = Output::Backtrace.first(@identity)
174
182
  identity = @identity&.scoped
183
+ backtrace = Output::Backtrace.first(identity)
184
+ assert = Assert.new(identity, backtrace, self)
175
185
 
176
186
  if condition
177
- @passed << Assert.new(identity, self)
187
+ @passed << assert
178
188
 
179
189
  if !@orientation || @verbose
180
- @output.puts(:indent, *pass_prefix, message || "assertion passed", backtrace)
190
+ @output.assert(condition, @orientation, message || "assertion passed", backtrace)
181
191
  end
182
192
  else
183
- @failed << Assert.new(identity, self)
193
+ @failed << assert
184
194
 
185
195
  if @orientation || @verbose
186
- @output.puts(:indent, *fail_prefix, message || "assertion failed", backtrace)
196
+ @output.assert(condition, @orientation, message || "assertion failed", backtrace)
187
197
  end
188
198
  end
189
199
  end
190
200
 
201
+ def message
202
+ {
203
+ text: @output.string,
204
+ location: @identity&.to_location
205
+ }
206
+ end
207
+
191
208
  def each_failure(&block)
192
209
  return to_enum(__method__) unless block_given?
193
210
 
194
- # if self.failed? and @identity
195
- # yield self
196
- # end
211
+ if self.failed? and @distinct
212
+ return yield(self)
213
+ end
197
214
 
198
215
  @failed.each do |assertions|
199
216
  assertions.each_failure(&block)
@@ -205,12 +222,21 @@ module Sus
205
222
  end
206
223
 
207
224
  def skip(reason)
208
- @output.puts(:indent, :skipped, skip_prefix, reason)
225
+ @output.skip(reason, @identity&.scoped)
226
+
209
227
  @skipped << self
210
228
  end
211
229
 
212
- def inform(message)
213
- @output.puts(:indent, :inform, inform_prefix, message)
230
+ def inform(message = nil)
231
+ if message.nil? and block_given?
232
+ begin
233
+ message = yield
234
+ rescue => error
235
+ message = error.full_message
236
+ end
237
+ end
238
+
239
+ @output.inform(message, @identity&.scoped)
214
240
  end
215
241
 
216
242
  # Add deferred assertions.
@@ -247,33 +273,26 @@ module Sus
247
273
 
248
274
  def message
249
275
  {
250
- text: @error.message,
276
+ text: @error.full_message,
251
277
  location: @identity&.to_location
252
278
  }
253
279
  end
254
280
  end
255
281
 
256
282
  def error!(error)
257
- identity = @identity.scoped(error.backtrace_locations)
283
+ identity = @identity&.scoped(error.backtrace_locations)
258
284
 
259
285
  @errored << Error.new(identity, error)
260
286
 
261
- lines = error.message.split(/\r?\n/)
262
-
263
- @output.puts(:indent, *error_prefix, error.class, ": ", lines.shift)
264
-
265
- lines.each do |line|
266
- @output.puts(:indent, line)
267
- end
268
-
269
- @output.write(Output::Backtrace.for(error, @identity))
287
+ # TODO consider passing `identity`.
288
+ @output.error(error, @identity)
270
289
  end
271
290
 
272
- def nested(target, identity: @identity, isolated: false, buffered: false, inverted: false, **options)
291
+ def nested(target, identity: nil, isolated: false, distinct: false, inverted: false, **options)
273
292
  result = nil
274
293
 
275
294
  # Isolated assertions need to have buffered output so they can be replayed if they fail:
276
- if isolated or buffered
295
+ if isolated or distinct
277
296
  output = @output.buffered
278
297
  else
279
298
  output = @output
@@ -288,7 +307,7 @@ module Sus
288
307
 
289
308
  output.puts(:indent, target)
290
309
 
291
- assertions = self.class.new(identity: identity, target: target, output: output, isolated: isolated, inverted: inverted, orientation: orientation, verbose: @verbose, **options)
310
+ assertions = self.class.new(identity: identity, target: target, output: output, isolated: isolated, inverted: inverted, orientation: orientation, distinct: distinct, verbose: @verbose, **options)
292
311
 
293
312
  output.indented do
294
313
  begin
@@ -309,15 +328,17 @@ module Sus
309
328
  # All child assertions should be resolved by this point:
310
329
  raise "Nested assertions must be fully resolved!" if assertions.deferred?
311
330
 
312
- if assertions.isolated or assertions.inverted
331
+ if assertions.append?
313
332
  # If we are isolated, we merge all child assertions into the parent as a single entity:
314
- merge!(assertions)
333
+ append!(assertions)
315
334
  else
316
335
  # Otherwise, we append all child assertions into the parent assertions:
317
- append!(assertions)
336
+ merge!(assertions)
318
337
  end
319
338
  end
320
339
 
340
+ protected
341
+
321
342
  def resolve_into(parent)
322
343
  # If the assertions should be an isolated group, make sure any deferred assertions are resolved:
323
344
  if @isolated and self.deferred?
@@ -339,9 +360,14 @@ module Sus
339
360
  end
340
361
  end
341
362
 
363
+ # Whether the child assertions should be merged into the parent assertions.
364
+ def append?
365
+ @isolated || @inverted || @distinct
366
+ end
367
+
342
368
  private
343
369
 
344
- def merge!(assertions)
370
+ def append!(assertions)
345
371
  @count += assertions.count
346
372
 
347
373
  if assertions.errored?
@@ -363,7 +389,8 @@ module Sus
363
389
  end
364
390
  end
365
391
 
366
- def append!(assertions)
392
+ # Concatenate the child assertions into this instance.
393
+ def merge!(assertions)
367
394
  @count += assertions.count
368
395
  @passed.concat(assertions.passed)
369
396
  @failed.concat(assertions.failed)
@@ -377,36 +404,5 @@ module Sus
377
404
  # @output.puts
378
405
  # end
379
406
  end
380
-
381
- PASSED_PREFIX = [:passed, "✓ "].freeze
382
- FAILED_PREFIX = [:failed, "✗ "].freeze
383
-
384
- def pass_prefix
385
- if @orientation
386
- PASSED_PREFIX
387
- else
388
- FAILED_PREFIX
389
- end
390
- end
391
-
392
- def fail_prefix
393
- if @orientation
394
- FAILED_PREFIX
395
- else
396
- PASSED_PREFIX
397
- end
398
- end
399
-
400
- def inform_prefix
401
- "ℹ "
402
- end
403
-
404
- def skip_prefix
405
- "⏸ "
406
- end
407
-
408
- def error_prefix
409
- [:errored, "⚠ "]
410
- end
411
407
  end
412
408
  end
data/lib/sus/expect.rb CHANGED
@@ -5,16 +5,15 @@
5
5
 
6
6
  module Sus
7
7
  class Expect
8
- def initialize(assertions, subject, inverted: false, buffered: false)
8
+ def initialize(assertions, subject, inverted: false, distinct: false)
9
9
  @assertions = assertions
10
10
  @subject = subject
11
11
  @inverted = inverted
12
- @buffered = buffered
12
+ @distinct = true
13
13
  end
14
14
 
15
15
  attr :subject
16
16
  attr :inverted
17
- attr :assertions
18
17
 
19
18
  def not
20
19
  self.dup.tap do |expect|
@@ -36,7 +35,7 @@ module Sus
36
35
  # This gets the identity scoped to the current call stack, which ensures that any failures are logged at this point in the code.
37
36
  identity = @assertions.identity&.scoped
38
37
 
39
- @assertions.nested(self, inverted: @inverted, buffered: @buffered, identity: identity) do |assertions|
38
+ @assertions.nested(self, inverted: @inverted, identity: identity, distinct: @distinct) do |assertions|
40
39
  predicate.call(assertions, @subject)
41
40
  end
42
41
 
@@ -51,9 +50,9 @@ module Sus
51
50
  class Base
52
51
  def expect(subject = nil, &block)
53
52
  if block_given?
54
- Expect.new(@__assertions__, block, buffered: true)
53
+ Expect.new(@__assertions__, block, distinct: true)
55
54
  else
56
- Expect.new(@__assertions__, subject, buffered: true)
55
+ Expect.new(@__assertions__, subject, distinct: true)
57
56
  end
58
57
  end
59
58
  end
data/lib/sus/it.rb CHANGED
@@ -45,13 +45,9 @@ module Sus
45
45
  end
46
46
 
47
47
  def handle_skip(instance, assertions)
48
- reason = catch(:skip) do
48
+ catch(:skip) do
49
49
  return instance.call
50
50
  end
51
-
52
- assertions.skip(reason)
53
-
54
- return nil
55
51
  end
56
52
  end
57
53
 
@@ -62,7 +58,10 @@ module Sus
62
58
  end
63
59
 
64
60
  class Base
61
+ # Skip the current test with a reason.
62
+ # @parameter reason [String] The reason for skipping the test.
65
63
  def skip(reason)
64
+ @__assertions__.skip(reason)
66
65
  throw :skip, reason
67
66
  end
68
67
  end
@@ -75,6 +75,26 @@ module Sus
75
75
  @chunks << [:puts, *arguments]
76
76
  @tee&.puts(*arguments)
77
77
  end
78
+
79
+ def assert(*arguments)
80
+ @chunks << [:assert, *arguments]
81
+ @tee&.assert(*arguments)
82
+ end
83
+
84
+ def skip(*arguments)
85
+ @chunks << [:skip, *arguments]
86
+ @tee&.skip(*arguments)
87
+ end
88
+
89
+ def error(*arguments)
90
+ @chunks << [:error, *arguments]
91
+ @tee&.error(*arguments)
92
+ end
93
+
94
+ def inform(*arguments)
95
+ @chunks << [:inform, *arguments]
96
+ @tee&.inform(*arguments)
97
+ end
78
98
  end
79
99
  end
80
100
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Sus
7
+ # Styled output output.
8
+ module Output
9
+ module Messages
10
+ PASSED_PREFIX = [:passed, "✓ "].freeze
11
+ FAILED_PREFIX = [:failed, "✗ "].freeze
12
+
13
+ def pass_prefix(orientation)
14
+ if orientation
15
+ PASSED_PREFIX
16
+ else
17
+ FAILED_PREFIX
18
+ end
19
+ end
20
+
21
+ def fail_prefix(orientation)
22
+ if orientation
23
+ FAILED_PREFIX
24
+ else
25
+ PASSED_PREFIX
26
+ end
27
+ end
28
+
29
+ # If the orientation is true, and the test passed, then it is a successful outcome.
30
+ # If the orientation is false, and the test failed, then it is a successful outcome.
31
+ # Otherwise, it is a failed outcome.
32
+ #
33
+ # @parameter condition [Boolean] The result of the test.
34
+ # @parameter orientation [Boolean] The orientation of the assertions.
35
+ # @parameter message [String] The message to display.
36
+ # @parameter backtrace [Array] The backtrace to display.
37
+ def assert(condition, orientation, message, backtrace)
38
+ if condition
39
+ self.puts(:indent, *pass_prefix(orientation), message, backtrace)
40
+ else
41
+ self.puts(:indent, *fail_prefix(orientation), message, backtrace)
42
+ end
43
+ end
44
+
45
+ def skip_prefix
46
+ "⏸ "
47
+ end
48
+
49
+ def skip(reason, identity)
50
+ self.puts(:indent, :skipped, skip_prefix, reason)
51
+ end
52
+
53
+ def error_prefix
54
+ [:errored, "⚠ "]
55
+ end
56
+
57
+ def error(error, identity)
58
+ lines = error.message.split(/\r?\n/)
59
+
60
+ self.puts(:indent, *error_prefix, error.class, ": ", lines.shift)
61
+
62
+ lines.each do |line|
63
+ self.puts(:indent, line)
64
+ end
65
+
66
+ self.write(Output::Backtrace.for(error, identity))
67
+ end
68
+
69
+ def inform_prefix
70
+ "ℹ "
71
+ end
72
+
73
+ def inform(message, identity)
74
+ self.puts(:indent, :inform, inform_prefix, message)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -3,13 +3,14 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2022, by Samuel Williams.
5
5
 
6
- require 'io/console'
7
- require 'stringio'
6
+ require_relative 'messages'
8
7
 
9
8
  module Sus
10
9
  # Styled output output.
11
10
  module Output
12
11
  class Null
12
+ include Messages
13
+
13
14
  def initialize
14
15
  end
15
16
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'null'
7
+
8
+ module Sus
9
+ # Styled output output.
10
+ module Output
11
+ class Structured < Null
12
+ def self.buffered(...)
13
+ Buffered.new(self.new(...))
14
+ end
15
+
16
+ def initialize(io, identity = nil)
17
+ @io = io
18
+ @identity = identity
19
+ end
20
+
21
+ def skip(reason, identity)
22
+ inform(reason.to_s, identity)
23
+ end
24
+
25
+ def inform(message, identity)
26
+ unless message.is_a?(String)
27
+ message = message.inspect
28
+ end
29
+
30
+ @io.puts(JSON.generate({
31
+ inform: @identity,
32
+ message: {
33
+ text: message,
34
+ location: identity&.to_location,
35
+ }
36
+ }))
37
+
38
+ @io.flush
39
+ end
40
+ end
41
+ end
42
+ end
@@ -3,13 +3,14 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2022, by Samuel Williams.
5
5
 
6
- require 'io/console'
6
+ require_relative 'messages'
7
7
  require_relative 'buffered'
8
8
 
9
9
  module Sus
10
- # Styled io io.
11
10
  module Output
12
11
  class Text
12
+ include Messages
13
+
13
14
  def initialize(io)
14
15
  @io = io
15
16
 
data/lib/sus/version.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2022, by Samuel Williams.
5
5
 
6
6
  module Sus
7
- VERSION = "0.20.2"
7
+ VERSION = "0.21.0"
8
8
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.2
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -38,7 +38,7 @@ cert_chain:
38
38
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
39
39
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
40
40
  -----END CERTIFICATE-----
41
- date: 2023-03-01 00:00:00.000000000 Z
41
+ date: 2023-06-11 00:00:00.000000000 Z
42
42
  dependencies:
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: bake-test
@@ -125,9 +125,11 @@ files:
125
125
  - lib/sus/output/bar.rb
126
126
  - lib/sus/output/buffered.rb
127
127
  - lib/sus/output/lines.rb
128
+ - lib/sus/output/messages.rb
128
129
  - lib/sus/output/null.rb
129
130
  - lib/sus/output/progress.rb
130
131
  - lib/sus/output/status.rb
132
+ - lib/sus/output/structured.rb
131
133
  - lib/sus/output/text.rb
132
134
  - lib/sus/output/xterm.rb
133
135
  - lib/sus/raise_exception.rb
@@ -160,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
162
  - !ruby/object:Gem::Version
161
163
  version: '0'
162
164
  requirements: []
163
- rubygems_version: 3.4.6
165
+ rubygems_version: 3.4.7
164
166
  signing_key:
165
167
  specification_version: 4
166
168
  summary: A fast and scalable test runner.
metadata.gz.sig CHANGED
Binary file