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.
@@ -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,11 @@
1
+ module TestBench
2
+ class Output
3
+ class Writer
4
+ module Defaults
5
+ def self.device
6
+ STDOUT
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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