test_bench-output 2.0.0.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/lib/test_bench/output/controls/data.rb +42 -0
- data/lib/test_bench/output/controls/detail.rb +22 -0
- data/lib/test_bench/output/controls/device.rb +27 -0
- data/lib/test_bench/output/controls/events.rb +7 -0
- data/lib/test_bench/output/controls/output.rb +45 -0
- data/lib/test_bench/output/controls/random.rb +7 -0
- data/lib/test_bench/output/controls/result.rb +7 -0
- data/lib/test_bench/output/controls/style.rb +31 -0
- data/lib/test_bench/output/controls/styling.rb +22 -0
- data/lib/test_bench/output/controls/text.rb +32 -0
- data/lib/test_bench/output/controls.rb +12 -0
- data/lib/test_bench/output/device/null.rb +19 -0
- data/lib/test_bench/output/device/substitute.rb +61 -0
- data/lib/test_bench/output/digest.rb +91 -0
- data/lib/test_bench/output/output.rb +335 -0
- data/lib/test_bench/output/writer/buffer/console.rb +184 -0
- data/lib/test_bench/output/writer/buffer.rb +48 -0
- data/lib/test_bench/output/writer/defaults.rb +11 -0
- data/lib/test_bench/output/writer/style.rb +50 -0
- data/lib/test_bench/output/writer/substitute.rb +28 -0
- data/lib/test_bench/output/writer.rb +266 -0
- data/lib/test_bench/output.rb +17 -0
- metadata +92 -0
@@ -0,0 +1,335 @@
|
|
1
|
+
module TestBench
|
2
|
+
class Output
|
3
|
+
include Session::Handler
|
4
|
+
|
5
|
+
def pending_writer
|
6
|
+
@pending_writer ||= Writer::Substitute.build
|
7
|
+
end
|
8
|
+
attr_writer :pending_writer
|
9
|
+
|
10
|
+
def passing_writer
|
11
|
+
@passing_writer ||= Writer::Substitute.build
|
12
|
+
end
|
13
|
+
attr_writer :passing_writer
|
14
|
+
|
15
|
+
def failing_writer
|
16
|
+
@failing_writer ||= Writer::Substitute.build
|
17
|
+
end
|
18
|
+
attr_writer :failing_writer
|
19
|
+
|
20
|
+
def failures
|
21
|
+
@failures ||= []
|
22
|
+
end
|
23
|
+
attr_writer :failures
|
24
|
+
|
25
|
+
def mode
|
26
|
+
@mode ||= Mode.initial
|
27
|
+
end
|
28
|
+
attr_writer :mode
|
29
|
+
|
30
|
+
def failures
|
31
|
+
@failures ||= []
|
32
|
+
end
|
33
|
+
attr_writer :failures
|
34
|
+
|
35
|
+
def branch_count
|
36
|
+
@branch_count ||= 0
|
37
|
+
end
|
38
|
+
attr_writer :branch_count
|
39
|
+
|
40
|
+
def detail_policy
|
41
|
+
@detail_policy ||= Detail.default
|
42
|
+
end
|
43
|
+
alias :detail :detail_policy
|
44
|
+
attr_writer :detail_policy
|
45
|
+
|
46
|
+
def self.build(device: nil, styling: nil, detail: nil)
|
47
|
+
instance = new
|
48
|
+
|
49
|
+
Writer.configure(instance, device:, styling:, attr_name: :pending_writer)
|
50
|
+
|
51
|
+
if not detail.nil?
|
52
|
+
instance.detail_policy = detail
|
53
|
+
end
|
54
|
+
|
55
|
+
instance
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.register(telemetry, **arguments)
|
59
|
+
instance = build(**arguments)
|
60
|
+
|
61
|
+
telemetry.register(instance)
|
62
|
+
instance
|
63
|
+
end
|
64
|
+
|
65
|
+
def receive(event)
|
66
|
+
case event
|
67
|
+
when ContextStarted, TestStarted
|
68
|
+
branch
|
69
|
+
end
|
70
|
+
|
71
|
+
if initial?
|
72
|
+
handle(event)
|
73
|
+
|
74
|
+
else
|
75
|
+
self.mode = Mode.failing
|
76
|
+
handle(event)
|
77
|
+
|
78
|
+
self.mode = Mode.passing
|
79
|
+
handle(event)
|
80
|
+
|
81
|
+
self.mode = Mode.pending
|
82
|
+
handle(event)
|
83
|
+
end
|
84
|
+
|
85
|
+
case event
|
86
|
+
when ContextFinished, TestFinished
|
87
|
+
merge(event.result)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
handle Detailed do |text, quote, heading|
|
92
|
+
if not detail?
|
93
|
+
return
|
94
|
+
end
|
95
|
+
|
96
|
+
comment(text, quote, heading)
|
97
|
+
end
|
98
|
+
|
99
|
+
handle Commented do |text, quote, heading|
|
100
|
+
comment(text, quote, heading)
|
101
|
+
end
|
102
|
+
|
103
|
+
handle ContextStarted do |title|
|
104
|
+
if not title.nil?
|
105
|
+
writer.
|
106
|
+
indent.
|
107
|
+
style(:green).
|
108
|
+
puts(title)
|
109
|
+
|
110
|
+
writer.indent!
|
111
|
+
|
112
|
+
if branch_count == 1
|
113
|
+
failures.clear
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
handle ContextFinished do |title|
|
119
|
+
if not title.nil?
|
120
|
+
writer.deindent!
|
121
|
+
|
122
|
+
if branch_count == 1
|
123
|
+
writer.puts
|
124
|
+
|
125
|
+
if failing? && failures.any?
|
126
|
+
writer.
|
127
|
+
style(:bold, :red).
|
128
|
+
puts("Failure#{'s' if not failures.one?}:")
|
129
|
+
|
130
|
+
failures.each do |failure_message|
|
131
|
+
writer.
|
132
|
+
print('- ').
|
133
|
+
style(:red).
|
134
|
+
puts(failure_message)
|
135
|
+
end
|
136
|
+
|
137
|
+
writer.puts
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
handle TestStarted do |title|
|
144
|
+
if title.nil?
|
145
|
+
if passing?
|
146
|
+
return
|
147
|
+
else
|
148
|
+
title = 'Test'
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
writer.indent
|
153
|
+
|
154
|
+
if passing?
|
155
|
+
writer.style(:green)
|
156
|
+
elsif failing?
|
157
|
+
if not writer.styling?
|
158
|
+
title = "#{title} (failed)"
|
159
|
+
end
|
160
|
+
|
161
|
+
writer.style(:bold, :red)
|
162
|
+
elsif pending?
|
163
|
+
writer.style(:faint)
|
164
|
+
end
|
165
|
+
|
166
|
+
writer.puts(title)
|
167
|
+
|
168
|
+
writer.indent!
|
169
|
+
end
|
170
|
+
|
171
|
+
handle TestFinished do |title, result|
|
172
|
+
if passing? && title.nil?
|
173
|
+
return
|
174
|
+
end
|
175
|
+
|
176
|
+
writer.deindent!
|
177
|
+
end
|
178
|
+
|
179
|
+
handle Failed do |message|
|
180
|
+
if failing?
|
181
|
+
failures << message
|
182
|
+
end
|
183
|
+
|
184
|
+
writer
|
185
|
+
.indent
|
186
|
+
.style(:red)
|
187
|
+
.puts(message)
|
188
|
+
end
|
189
|
+
|
190
|
+
def comment(text, quote, heading)
|
191
|
+
if not heading.nil?
|
192
|
+
writer.style(:bold, :underline).puts(heading)
|
193
|
+
end
|
194
|
+
|
195
|
+
if text.empty?
|
196
|
+
writer.
|
197
|
+
indent.
|
198
|
+
style(:faint, :italic).
|
199
|
+
puts('(empty)')
|
200
|
+
return
|
201
|
+
end
|
202
|
+
|
203
|
+
if not quote
|
204
|
+
writer.puts(text)
|
205
|
+
else
|
206
|
+
text.each_line do |line|
|
207
|
+
line.chomp!
|
208
|
+
|
209
|
+
writer.
|
210
|
+
indent.
|
211
|
+
style(:faint).
|
212
|
+
print('> ').
|
213
|
+
style(:reset_intensity).
|
214
|
+
puts(line)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def current_writer
|
220
|
+
if initial? || pending?
|
221
|
+
pending_writer
|
222
|
+
elsif passing?
|
223
|
+
passing_writer
|
224
|
+
elsif failing?
|
225
|
+
failing_writer
|
226
|
+
end
|
227
|
+
end
|
228
|
+
alias :writer :current_writer
|
229
|
+
|
230
|
+
def branch
|
231
|
+
if branch_count.zero?
|
232
|
+
self.mode = Mode.pending
|
233
|
+
|
234
|
+
pending_writer.sync = false
|
235
|
+
|
236
|
+
parent_writer = pending_writer
|
237
|
+
else
|
238
|
+
parent_writer = passing_writer
|
239
|
+
end
|
240
|
+
|
241
|
+
self.branch_count += 1
|
242
|
+
|
243
|
+
self.passing_writer, self.failing_writer = parent_writer.branch
|
244
|
+
end
|
245
|
+
|
246
|
+
def merge(result)
|
247
|
+
self.branch_count -= 1
|
248
|
+
|
249
|
+
if not branched?
|
250
|
+
pending_writer.sync = true
|
251
|
+
|
252
|
+
self.mode = Mode.initial
|
253
|
+
end
|
254
|
+
|
255
|
+
if result
|
256
|
+
writer = passing_writer
|
257
|
+
else
|
258
|
+
writer = failing_writer
|
259
|
+
end
|
260
|
+
|
261
|
+
writer.flush
|
262
|
+
|
263
|
+
self.passing_writer = writer.device
|
264
|
+
self.failing_writer = writer.alternate_device
|
265
|
+
end
|
266
|
+
|
267
|
+
def branched?
|
268
|
+
branch_count > 0
|
269
|
+
end
|
270
|
+
|
271
|
+
def initial?
|
272
|
+
mode == Mode.initial
|
273
|
+
end
|
274
|
+
|
275
|
+
def pending?
|
276
|
+
mode == Mode.pending
|
277
|
+
end
|
278
|
+
|
279
|
+
def passing?
|
280
|
+
mode == Mode.passing
|
281
|
+
end
|
282
|
+
|
283
|
+
def failing?
|
284
|
+
mode == Mode.failing
|
285
|
+
end
|
286
|
+
|
287
|
+
def detail?
|
288
|
+
Detail.detail?(detail_policy, mode)
|
289
|
+
end
|
290
|
+
|
291
|
+
module Mode
|
292
|
+
def self.initial = :initial
|
293
|
+
def self.pending = :pending
|
294
|
+
def self.passing = :passing
|
295
|
+
def self.failing = :failing
|
296
|
+
end
|
297
|
+
|
298
|
+
module Detail
|
299
|
+
Error = Class.new(RuntimeError)
|
300
|
+
|
301
|
+
def self.detail?(policy, mode)
|
302
|
+
case policy
|
303
|
+
when on
|
304
|
+
true
|
305
|
+
when off
|
306
|
+
false
|
307
|
+
when failure
|
308
|
+
if mode == Mode.failing || mode == Mode.initial
|
309
|
+
true
|
310
|
+
else
|
311
|
+
false
|
312
|
+
end
|
313
|
+
else
|
314
|
+
raise Error, "Unknown detail policy #{policy.inspect}"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def self.on = :on
|
319
|
+
def self.off = :off
|
320
|
+
def self.failure = :failure
|
321
|
+
|
322
|
+
def self.default
|
323
|
+
policy = ENV.fetch('TEST_BENCH_DETAIL') do
|
324
|
+
return default!
|
325
|
+
end
|
326
|
+
|
327
|
+
policy.to_sym
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.default!
|
331
|
+
:failure
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module TestBench
|
2
|
+
class Output
|
3
|
+
class Writer
|
4
|
+
class Buffer
|
5
|
+
class Console
|
6
|
+
def device
|
7
|
+
@device ||= Device::Null.build
|
8
|
+
end
|
9
|
+
attr_writer :device
|
10
|
+
|
11
|
+
def geometry
|
12
|
+
@geometry ||= Geometry.get
|
13
|
+
end
|
14
|
+
attr_writer :geometry
|
15
|
+
|
16
|
+
def cursor_saved
|
17
|
+
@cursor_saved ||= false
|
18
|
+
end
|
19
|
+
alias :cursor_saved? :cursor_saved
|
20
|
+
attr_writer :cursor_saved
|
21
|
+
|
22
|
+
def self.build(device=nil)
|
23
|
+
device ||= Defaults.device
|
24
|
+
|
25
|
+
instance = new
|
26
|
+
|
27
|
+
if device.tty?
|
28
|
+
instance.device = device
|
29
|
+
end
|
30
|
+
|
31
|
+
instance
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configure(receiver, device: nil, attr_name: nil)
|
35
|
+
attr_name ||= :buffer
|
36
|
+
|
37
|
+
instance = build(device)
|
38
|
+
receiver.public_send(:"#{attr_name}=", instance)
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_geometry(width, height, row, column)
|
42
|
+
geometry = Geometry.new(width, height, row, column)
|
43
|
+
self.geometry = geometry
|
44
|
+
end
|
45
|
+
|
46
|
+
def receive(text)
|
47
|
+
if not cursor_saved?
|
48
|
+
device.write("\e[s")
|
49
|
+
self.cursor_saved = true
|
50
|
+
end
|
51
|
+
|
52
|
+
write_ahead_text = geometry.next(text)
|
53
|
+
|
54
|
+
if not write_ahead_text.empty?
|
55
|
+
device.write(write_ahead_text)
|
56
|
+
elsif geometry.bottom_row?
|
57
|
+
buffering_message = "Output is buffering"
|
58
|
+
|
59
|
+
device.write("\e[2m#{buffering_message}\e[22m")
|
60
|
+
geometry.next!(buffering_message)
|
61
|
+
end
|
62
|
+
|
63
|
+
device.flush
|
64
|
+
|
65
|
+
write_ahead_text.length
|
66
|
+
end
|
67
|
+
|
68
|
+
def flush(...)
|
69
|
+
if cursor_saved?
|
70
|
+
device.write("\e[u")
|
71
|
+
self.cursor_saved = false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def capacity
|
76
|
+
geometry.capacity
|
77
|
+
end
|
78
|
+
|
79
|
+
def next_position(text)
|
80
|
+
geometry.next_position(text)
|
81
|
+
end
|
82
|
+
|
83
|
+
Geometry = Struct.new(:width, :height, :row, :column) do
|
84
|
+
def self.get
|
85
|
+
instance = new
|
86
|
+
|
87
|
+
STDIN.raw do |stdin|
|
88
|
+
instance.height, instance.width = stdin.winsize
|
89
|
+
|
90
|
+
instance.row, instance.column = stdin.cursor
|
91
|
+
end
|
92
|
+
|
93
|
+
instance
|
94
|
+
end
|
95
|
+
|
96
|
+
def bottom_row?
|
97
|
+
row + 1 == height && column == 0
|
98
|
+
end
|
99
|
+
|
100
|
+
def next(text)
|
101
|
+
write_ahead_text = text.slice!(0, capacity)
|
102
|
+
|
103
|
+
next!(write_ahead_text)
|
104
|
+
|
105
|
+
write_ahead_text
|
106
|
+
end
|
107
|
+
|
108
|
+
def next!(text)
|
109
|
+
escape_sequence_pattern = %r{\A\e\[[[:digit:]]+(?:;[[:digit:]]+)*[[:alpha:]]$}
|
110
|
+
if escape_sequence_pattern.match?(text)
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
row, column, _scroll_rows = next_position(text)
|
115
|
+
|
116
|
+
self.row = row
|
117
|
+
self.column = column
|
118
|
+
end
|
119
|
+
|
120
|
+
def capacity
|
121
|
+
capacity = 0
|
122
|
+
|
123
|
+
rows_remaining = height - row - 1
|
124
|
+
|
125
|
+
if rows_remaining > 0
|
126
|
+
capacity += (rows_remaining - 1) * width
|
127
|
+
|
128
|
+
final_row = width - column
|
129
|
+
capacity += final_row
|
130
|
+
end
|
131
|
+
|
132
|
+
capacity
|
133
|
+
end
|
134
|
+
|
135
|
+
def next_position(text)
|
136
|
+
text_length = text.length
|
137
|
+
|
138
|
+
newline = text.end_with?("\n")
|
139
|
+
if newline
|
140
|
+
text_length -= 1
|
141
|
+
end
|
142
|
+
|
143
|
+
row = self.row
|
144
|
+
column = self.column
|
145
|
+
|
146
|
+
text_rows, text_columns = text_length.divmod(width)
|
147
|
+
|
148
|
+
row += text_rows
|
149
|
+
|
150
|
+
columns_remaining = width - column
|
151
|
+
if columns_remaining > text_columns
|
152
|
+
column += text_columns
|
153
|
+
else
|
154
|
+
row += 1
|
155
|
+
column = text_columns - columns_remaining
|
156
|
+
end
|
157
|
+
|
158
|
+
if newline
|
159
|
+
reached_next_line = column == 0 && row > self.row
|
160
|
+
|
161
|
+
newline_needed = !reached_next_line
|
162
|
+
if newline_needed
|
163
|
+
row += 1
|
164
|
+
column = 0
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
if row >= height
|
169
|
+
row_limit = height - 1
|
170
|
+
|
171
|
+
scroll_rows = row - row_limit
|
172
|
+
row = row_limit
|
173
|
+
else
|
174
|
+
scroll_rows = 0
|
175
|
+
end
|
176
|
+
|
177
|
+
return row, column, scroll_rows
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module TestBench
|
2
|
+
class Output
|
3
|
+
class Writer
|
4
|
+
class Buffer
|
5
|
+
attr_accessor :limit
|
6
|
+
|
7
|
+
def contents
|
8
|
+
@contents ||= String.new
|
9
|
+
end
|
10
|
+
attr_writer :contents
|
11
|
+
|
12
|
+
def receive(data)
|
13
|
+
bytes = data.bytesize
|
14
|
+
|
15
|
+
if not limit.nil?
|
16
|
+
final_size = contents.bytesize + data.bytesize
|
17
|
+
|
18
|
+
if final_size > limit
|
19
|
+
bytes = limit - contents.bytesize
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
data = data[0...bytes]
|
24
|
+
|
25
|
+
contents << data
|
26
|
+
|
27
|
+
bytes
|
28
|
+
end
|
29
|
+
|
30
|
+
def limit?
|
31
|
+
!limit.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
def flush(device=nil, alternate_device=nil)
|
35
|
+
if not device.nil?
|
36
|
+
device.write(contents)
|
37
|
+
end
|
38
|
+
|
39
|
+
if not alternate_device.nil?
|
40
|
+
alternate_device.write(contents)
|
41
|
+
end
|
42
|
+
|
43
|
+
contents.clear
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module TestBench
|
2
|
+
class Output
|
3
|
+
class Writer
|
4
|
+
module Style
|
5
|
+
Error = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
def self.control_code(style)
|
8
|
+
control_codes.fetch(style) do
|
9
|
+
raise Error, "Invalid style #{style.inspect}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.control_codes
|
14
|
+
@sgr_codes ||= {
|
15
|
+
:reset => '0',
|
16
|
+
|
17
|
+
:bold => '1',
|
18
|
+
:faint => '2',
|
19
|
+
:italic => '3',
|
20
|
+
:underline => '4',
|
21
|
+
|
22
|
+
:reset_intensity => '22',
|
23
|
+
:reset_italic => '23',
|
24
|
+
:reset_underline => '24',
|
25
|
+
|
26
|
+
:black => '30',
|
27
|
+
:red => '31',
|
28
|
+
:green => '32',
|
29
|
+
:yellow => '33',
|
30
|
+
:blue => '34',
|
31
|
+
:magenta => '35',
|
32
|
+
:cyan => '36',
|
33
|
+
:white => '37',
|
34
|
+
:reset_fg => '39',
|
35
|
+
|
36
|
+
:black_bg => '40',
|
37
|
+
:red_bg => '41',
|
38
|
+
:green_bg => '42',
|
39
|
+
:yellow_bg => '43',
|
40
|
+
:blue_bg => '44',
|
41
|
+
:magenta_bg => '45',
|
42
|
+
:cyan_bg => '46',
|
43
|
+
:white_bg => '47',
|
44
|
+
:reset_bg => '49'
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module TestBench
|
2
|
+
class Output
|
3
|
+
class Writer
|
4
|
+
module Substitute
|
5
|
+
def self.build
|
6
|
+
Writer.build
|
7
|
+
end
|
8
|
+
|
9
|
+
class Writer < Writer
|
10
|
+
def written_data
|
11
|
+
device.written_data
|
12
|
+
end
|
13
|
+
alias :written_text :written_data
|
14
|
+
|
15
|
+
def self.build
|
16
|
+
instance = new
|
17
|
+
instance.buffer.limit = 0
|
18
|
+
instance
|
19
|
+
end
|
20
|
+
|
21
|
+
def styling!
|
22
|
+
self.styling_policy = Styling.on
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|