test_bench-output 2.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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