andyw8-seeing_is_believing 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +60 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +2 -0
  6. data/README.md +70 -0
  7. data/Rakefile +88 -0
  8. data/appveyor.yml +32 -0
  9. data/bin/seeing_is_believing +7 -0
  10. data/docs/example.gif +0 -0
  11. data/docs/frog-brown.png +0 -0
  12. data/docs/sib-streaming.gif +0 -0
  13. data/features/deprecated-flags.feature +91 -0
  14. data/features/errors.feature +155 -0
  15. data/features/examples.feature +423 -0
  16. data/features/flags.feature +852 -0
  17. data/features/regression.feature +898 -0
  18. data/features/support/env.rb +102 -0
  19. data/features/xmpfilter-style.feature +471 -0
  20. data/lib/seeing_is_believing/binary/align_chunk.rb +47 -0
  21. data/lib/seeing_is_believing/binary/align_file.rb +24 -0
  22. data/lib/seeing_is_believing/binary/align_line.rb +25 -0
  23. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +56 -0
  24. data/lib/seeing_is_believing/binary/annotate_every_line.rb +52 -0
  25. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +179 -0
  26. data/lib/seeing_is_believing/binary/comment_lines.rb +36 -0
  27. data/lib/seeing_is_believing/binary/commentable_lines.rb +126 -0
  28. data/lib/seeing_is_believing/binary/config.rb +455 -0
  29. data/lib/seeing_is_believing/binary/data_structures.rb +58 -0
  30. data/lib/seeing_is_believing/binary/engine.rb +161 -0
  31. data/lib/seeing_is_believing/binary/format_comment.rb +79 -0
  32. data/lib/seeing_is_believing/binary/interline_align.rb +57 -0
  33. data/lib/seeing_is_believing/binary/remove_annotations.rb +113 -0
  34. data/lib/seeing_is_believing/binary/rewrite_comments.rb +62 -0
  35. data/lib/seeing_is_believing/binary.rb +73 -0
  36. data/lib/seeing_is_believing/code.rb +139 -0
  37. data/lib/seeing_is_believing/compatibility.rb +28 -0
  38. data/lib/seeing_is_believing/debugger.rb +32 -0
  39. data/lib/seeing_is_believing/error.rb +17 -0
  40. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +195 -0
  41. data/lib/seeing_is_believing/event_stream/consumer.rb +221 -0
  42. data/lib/seeing_is_believing/event_stream/events.rb +193 -0
  43. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +61 -0
  44. data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
  45. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +23 -0
  46. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +41 -0
  47. data/lib/seeing_is_believing/event_stream/producer.rb +178 -0
  48. data/lib/seeing_is_believing/hard_core_ensure.rb +58 -0
  49. data/lib/seeing_is_believing/hash_struct.rb +206 -0
  50. data/lib/seeing_is_believing/result.rb +89 -0
  51. data/lib/seeing_is_believing/safe.rb +112 -0
  52. data/lib/seeing_is_believing/swap_files.rb +90 -0
  53. data/lib/seeing_is_believing/the_matrix.rb +97 -0
  54. data/lib/seeing_is_believing/version.rb +3 -0
  55. data/lib/seeing_is_believing/wrap_expressions.rb +265 -0
  56. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +19 -0
  57. data/lib/seeing_is_believing.rb +69 -0
  58. data/seeing_is_believing.gemspec +84 -0
  59. data/spec/binary/alignment_specs.rb +27 -0
  60. data/spec/binary/comment_lines_spec.rb +852 -0
  61. data/spec/binary/config_spec.rb +831 -0
  62. data/spec/binary/engine_spec.rb +114 -0
  63. data/spec/binary/format_comment_spec.rb +210 -0
  64. data/spec/binary/marker_spec.rb +71 -0
  65. data/spec/binary/remove_annotations_spec.rb +342 -0
  66. data/spec/binary/rewrite_comments_spec.rb +106 -0
  67. data/spec/code_spec.rb +233 -0
  68. data/spec/debugger_spec.rb +45 -0
  69. data/spec/evaluate_by_moving_files_spec.rb +204 -0
  70. data/spec/event_stream_spec.rb +762 -0
  71. data/spec/hard_core_ensure_spec.rb +120 -0
  72. data/spec/hash_struct_spec.rb +514 -0
  73. data/spec/seeing_is_believing_spec.rb +1094 -0
  74. data/spec/sib_spec_helpers/version.rb +17 -0
  75. data/spec/spec_helper.rb +26 -0
  76. data/spec/spec_helper_spec.rb +16 -0
  77. data/spec/wrap_expressions_spec.rb +1013 -0
  78. metadata +340 -0
@@ -0,0 +1,762 @@
1
+ # encoding: utf-8
2
+
3
+ require 'seeing_is_believing/event_stream/producer'
4
+ require 'seeing_is_believing/event_stream/consumer'
5
+ require 'seeing_is_believing/event_stream/handlers/debug'
6
+ require 'seeing_is_believing/debugger'
7
+
8
+ module SeeingIsBelieving::EventStream
9
+ RSpec.describe SeeingIsBelieving::EventStream do
10
+ attr_accessor :producer, :consumer
11
+ attr_accessor :eventstream_consumer, :eventstream_producer
12
+ attr_accessor :stdout_consumer, :stdout_producer
13
+ attr_accessor :stderr_consumer, :stderr_producer
14
+
15
+ def close_streams(*streams)
16
+ streams.each { |fd| fd.close unless fd.closed? }
17
+ end
18
+
19
+ def finish!
20
+ producer.finish!
21
+ consumer.process_exitstatus(0)
22
+ close_streams eventstream_producer, stdout_producer, stderr_producer
23
+ consumer.join
24
+ end
25
+
26
+ def inspected(obj)
27
+ Kernel.instance_method(:inspect).bind(obj).call
28
+ end
29
+
30
+
31
+ before do
32
+ self.eventstream_consumer, self.eventstream_producer = IO.pipe("utf-8")
33
+ self.stdout_consumer, self.stdout_producer = IO.pipe("utf-8")
34
+ self.stderr_consumer, self.stderr_producer = IO.pipe("utf-8")
35
+
36
+ self.producer = SeeingIsBelieving::EventStream::Producer.new eventstream_producer
37
+ self.consumer = SeeingIsBelieving::EventStream::Consumer.new \
38
+ events: eventstream_consumer,
39
+ stdout: stdout_consumer,
40
+ stderr: stderr_consumer
41
+ end
42
+
43
+ after do
44
+ finish!
45
+ close_streams eventstream_consumer, stdout_consumer, stderr_consumer
46
+ end
47
+
48
+ describe 'emitting an event' do
49
+ def has_message?(io)
50
+ readables, * = IO.select([io], [], [], 0.1) # 0.1 is the timeout
51
+ readables.to_a.any? # when it times out, IO.select may return nil...
52
+ end
53
+
54
+ it 'writes its events to the event stream' do
55
+ read, write = IO.pipe
56
+ producer = SeeingIsBelieving::EventStream::Producer.new(write)
57
+ expect(has_message? read).to eq false
58
+ producer.record_filename "whatever.rb"
59
+ expect(read.gets).to start_with 'filename'
60
+ end
61
+
62
+ # This test is irrelevant on MRI b/c of the GIL, but I ran it on Rbx to make sure it works
63
+ it 'is threadsafe as multiple events can occur at once' do
64
+ num_threads = 10
65
+ num_results = 600
66
+ line_nums_and_inspections = num_threads.times.flat_map { |line_num|
67
+ num_results.times.map { |value| "#{line_num}|#{value.inspect}" }
68
+ }
69
+
70
+ producer_threads = num_threads.times.map { |line_num|
71
+ Thread.new {
72
+ num_results.times { |value| producer.record_result :type, line_num, value }
73
+ }
74
+ }
75
+
76
+ (num_threads * num_results).times do |n|
77
+ result = consumer.call
78
+ ary_val = "#{result.line_number}|#{result.inspected}"
79
+ index = line_nums_and_inspections.index(ary_val)
80
+ raise "#{ary_val.inspect} is already consumed!" unless index
81
+ line_nums_and_inspections.delete_at index
82
+ end
83
+
84
+ expect(line_nums_and_inspections).to eq []
85
+ expect(producer_threads).to be_none(&:alive?)
86
+ end
87
+
88
+ it 'transcodes any received messages to UTF8' do
89
+ utf8 = "こんにちは" # from https://github.com/svenfuchs/i18n/blob/ee7fef8e9b9ee2f7d16e6c36d669ee7fb24ec613/lib/i18n/tests/interpolation.rb#L72
90
+ eucjp = utf8.encode(Encoding::EUCJP)
91
+ producer.record_sib_version(eucjp)
92
+ version = consumer.call.value
93
+ expect(version).to eq utf8
94
+ expect(version).to_not eq eucjp # general sanity checks to make
95
+ expect(utf8.bytes).to_not eq eucjp.bytes # sure I don't accidentally pass
96
+ end
97
+
98
+ def ascii8bit(str)
99
+ str.force_encoding Encoding::ASCII_8BIT
100
+ end
101
+
102
+ it 'force encodes the message to UTF8 when it can\'t validly transcode' do
103
+ producer.record_sib_version(ascii8bit("åß∂ƒ"))
104
+ version = consumer.call.value
105
+ expect(version).to eq "åß∂ƒ"
106
+ expect(version).to_not eq ascii8bit("åß∂ƒ")
107
+ end
108
+
109
+ it 'scrubs any invalid bytes to "�" when the force encoding isn\'t valid' do
110
+ producer.record_sib_version(ascii8bit "a\xFF å") # unicode bytes can't begin with
111
+ expect(consumer.call.value).to eq "a� å" # space just so its easier to see
112
+ end
113
+
114
+ it 'scrubs any invalid bytes to "�" when encoding was already UTF8, but was invalid' do
115
+ producer.record_sib_version("\xff√")
116
+ expect(consumer.call.value).to eq "�√"
117
+ end
118
+
119
+ it 'raises NoMoreEvents if input is closed before it finishes reading the number of requested inputs' do
120
+ finish!
121
+ expect { consumer.call 10 }.to raise_error SeeingIsBelieving::NoMoreEvents
122
+ end
123
+
124
+ it 'raises NoMoreEvents once its input streams are all closed and its seen an exit status' do
125
+ close_streams eventstream_producer, stdout_producer, stderr_producer
126
+ consumer.process_exitstatus 0
127
+ consumer.each { }
128
+ expect { consumer.call }.to raise_error SeeingIsBelieving::NoMoreEvents
129
+ end
130
+
131
+ it 'raises NoMoreEvents once its input streams are all closed and its seen a timeout' do
132
+ close_streams eventstream_producer, stdout_producer, stderr_producer
133
+ consumer.process_timeout 1
134
+ consumer.each { }
135
+ expect { consumer.call }.to raise_error SeeingIsBelieving::NoMoreEvents
136
+ end
137
+
138
+ it 'gracefully handles its side of the streams getting closed' do
139
+ close_streams eventstream_consumer, stdout_consumer, stderr_consumer
140
+ consumer.process_exitstatus 0
141
+ consumer.each { }
142
+ expect { consumer.call }.to raise_error SeeingIsBelieving::NoMoreEvents
143
+ end
144
+
145
+ specify 'if an incomprehensible event is received, it raises an UnknownEvent' do
146
+ eventstream_producer.puts "this is nonsense!"
147
+ expect{ consumer.call }.to raise_error SeeingIsBelieving::UnknownEvent, /nonsense/
148
+ end
149
+ end
150
+
151
+ describe 'each' do
152
+ it 'loops through and yields all events' do
153
+ # declare 2 events
154
+ producer.record_result :inspect, 100, 2
155
+ producer.record_sib_version('some ver')
156
+
157
+ # close streams so that it won't block waiting for more events
158
+ finish!
159
+
160
+ # record events
161
+ events = []
162
+ consumer.each { |e| events << e }
163
+
164
+ # it yielded the line result
165
+ line_result = events.find { |e| e.kind_of? Events::LineResult }
166
+ expect(line_result.line_number).to eq 100
167
+
168
+ # it yielded the version
169
+ version = events.find { |e| e.kind_of? Events::SiBVersion }
170
+ expect(version.value).to eq 'some ver'
171
+ end
172
+
173
+ it 'stops looping if there is no more input' do
174
+ producer.record_result :inspect, 100, 2
175
+ producer.record_sib_version('some ver')
176
+ finish!
177
+ expect(consumer.each.map { |e| e.class }.sort_by(&:to_s))
178
+ .to eq [ Events::EventStreamClosed, Events::Exitstatus, Events::Finished,
179
+ Events::LineResult, Events::SiBVersion, Events::StderrClosed, Events::StdoutClosed,
180
+ ]
181
+ end
182
+
183
+ it 'returns nil' do
184
+ finish!
185
+ expect(consumer.each { 1 }).to eq nil
186
+ end
187
+
188
+ it 'returns an enumerator if not given a block' do
189
+ producer.record_sib_version('some ver')
190
+ finish!
191
+ classes = consumer.each.map &:class
192
+ expect(classes).to include Events::SiBVersion
193
+ end
194
+ end
195
+
196
+
197
+ describe 'record_results' do
198
+ it 'emits a type, line_number, and escaped string' do
199
+ producer.record_result :type1, 123, [*'a'..'z', *'A'..'Z', *'0'..'9'].join("")
200
+ producer.record_result :type1, 123, '"'
201
+ producer.record_result :type1, 123, '""'
202
+ producer.record_result :type1, 123, "\n"
203
+ producer.record_result :type1, 123, "\r"
204
+ producer.record_result :type1, 123, "\n\r\n"
205
+ producer.record_result :type1, 123, "\#{}"
206
+ producer.record_result :type1, 123, [*0..127].map(&:chr).join("")
207
+ producer.record_result :type1, 123, "Ω≈ç√∫˜µ≤≥"
208
+
209
+ expect(consumer.call 9).to eq [
210
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: [*'a'..'z', *'A'..'Z', *'0'..'9'].join("").inspect),
211
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: '"'.inspect),
212
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: '""'.inspect),
213
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: "\n".inspect),
214
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: "\r".inspect),
215
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: "\n\r\n".inspect),
216
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: "\#{}".inspect),
217
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: [*0..127].map(&:chr).join("").inspect),
218
+ Events::LineResult.new(type: :type1, line_number: 123, inspected: "Ω≈ç√∫˜µ≤≥".inspect),
219
+ ]
220
+ end
221
+
222
+ it 'indicates that there are more results once it hits the max, but does not continue reporting them' do
223
+ producer.max_line_captures = 2
224
+
225
+ producer.record_result :type1, 123, 1
226
+ expect(consumer.call 1).to eq Events::LineResult.new(type: :type1, line_number: 123, inspected: '1')
227
+
228
+ producer.record_result :type1, 123, 2
229
+ expect(consumer.call 1).to eq Events::LineResult.new(type: :type1, line_number: 123, inspected: '2')
230
+
231
+ producer.record_result :type1, 123, 3
232
+ producer.record_result :type1, 123, 4
233
+ producer.record_result :type2, 123, 1
234
+ expect(consumer.call 2).to eq [Events::ResultsTruncated.new(type: :type1, line_number: 123),
235
+ Events::LineResult.new(type: :type2, line_number: 123, inspected: '1')]
236
+ end
237
+
238
+ it 'scopes the max to a given type/line' do
239
+ producer.max_line_captures = 1
240
+
241
+ producer.record_result :type1, 1, 1
242
+ producer.record_result :type1, 1, 2
243
+ producer.record_result :type1, 2, 3
244
+ producer.record_result :type1, 2, 4
245
+ producer.record_result :type2, 1, 5
246
+ producer.record_result :type2, 1, 6
247
+ expect(consumer.call 6).to eq [
248
+ Events::LineResult.new( type: :type1, line_number: 1, inspected: '1'),
249
+ Events::ResultsTruncated.new(type: :type1, line_number: 1),
250
+ Events::LineResult.new( type: :type1, line_number: 2, inspected: '3'),
251
+ Events::ResultsTruncated.new(type: :type1, line_number: 2),
252
+ Events::LineResult.new( type: :type2, line_number: 1, inspected: '5'),
253
+ Events::ResultsTruncated.new(type: :type2, line_number: 1),
254
+ ]
255
+ end
256
+
257
+ it 'returns the value' do
258
+ o = Object.new
259
+ expect(producer.record_result :type, 123, o).to equal o
260
+ end
261
+
262
+ # Some examples, mostly for the purpose of running individually if things get confusing
263
+ example 'Example: Simple' do
264
+ producer.record_result :type, 1, "a"
265
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: '"a"')
266
+
267
+ producer.record_result :type, 1, 1
268
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: '1')
269
+ end
270
+
271
+ example 'Example: Complex' do
272
+ str1 = (0...128).map(&:chr).join('') << "Ω≈ç√∫˜µ≤≥åß∂ƒ©˙∆˚¬…æœ∑´®†¥¨ˆøπ“‘¡™£¢ªº’”"
273
+ str2 = str1.dup
274
+ producer.record_result :type, 1, str2
275
+ expect(str2).to eq str1 # just making sure it doesn't mutate since this one is so complex
276
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: str1.inspect)
277
+ end
278
+
279
+ context 'calls #inspect when no block is given' do
280
+ it 'uses Kernel\'s inspect if there is no #inspect available e.g. BasicObject' do
281
+ obj = BasicObject.new
282
+ producer.record_result :type, 1, obj
283
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: inspected(obj))
284
+ end
285
+
286
+ it "uses Kernel's #inspect when the object\'s #inspect returns a not-String (e.g. pathalogical libraries like FactoryGirl)" do
287
+ obj = BasicObject.new
288
+ def obj.inspect
289
+ nil
290
+ end
291
+ producer.record_result :type, 1, obj
292
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: inspected(obj))
293
+ end
294
+
295
+ it "uses a null-inspect string when even Kernel's inspect doesn't work" do
296
+ skip 'uhm, no idea how to get it into such a state'
297
+ end
298
+
299
+ it 'only calls inspect once' do
300
+ count, obj = 0, Object.new
301
+ obj.define_singleton_method :inspect do
302
+ count += 1
303
+ 'a'
304
+ end
305
+ producer.record_result :type, 1, obj
306
+ expect(count).to eq 1
307
+ end
308
+
309
+ it 'can deal with results of inspect that have singleton methods' do
310
+ str = "a string"
311
+ def str.inspect() self end
312
+ producer.record_result :type, 1, str
313
+ expect(consumer.call.inspected).to eq str
314
+ end
315
+ end
316
+
317
+ context 'inspect performed by the block' do
318
+ it 'yields the object to the block and uses the block\'s result as the inspect value instead of calling inspect' do
319
+ o = Object.new
320
+ def o.inspect() 'real-inspect' end
321
+ def o.other_inspect() 'other-inspect' end
322
+ producer.record_result(:type, 1, o) { |x| x.other_inspect }
323
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: 'other-inspect')
324
+ end
325
+
326
+ it 'doesn\'t blow up if the block raises' do
327
+ o = Object.new
328
+ producer.record_result(:type, 1, o) { raise Exception, "zomg" }
329
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: inspected(o))
330
+ end
331
+
332
+ it 'doesn\'t blow up if the block returns a non-string' do
333
+ o = Object.new
334
+ producer.record_result(:type, 1, o) { nil }
335
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: inspected(o))
336
+
337
+ stringish = Object.new
338
+ def stringish.to_str() 'actual string' end
339
+ producer.record_result(:type, 1, o) { stringish }
340
+ expect(consumer.call).to eq Events::LineResult.new(type: :type, line_number: 1, inspected: 'actual string')
341
+ end
342
+
343
+ it 'invokes the block only once' do
344
+ o = Object.new
345
+ count = 0
346
+
347
+ producer.record_result(:type, 1, o) { count += 1 }
348
+ expect(count).to eq 1
349
+
350
+ producer.record_result(:type, 1, o) { count += 1; 'inspected-value' }
351
+ expect(count).to eq 2
352
+ end
353
+ end
354
+ end
355
+
356
+ describe 'max_line_captures (value and recording)' do
357
+ it 'is infinity by default' do
358
+ expect(producer.max_line_captures).to eq Float::INFINITY
359
+ end
360
+
361
+ it 'emits the event and sets the max_line_captures' do
362
+ producer.record_max_line_captures 123
363
+ expect(producer.max_line_captures).to eq 123
364
+ expect(consumer.call).to eq Events::MaxLineCaptures.new(value: 123)
365
+ end
366
+
367
+ it 'interprets numbers' do
368
+ producer.record_max_line_captures 12
369
+ expect(consumer.call).to eq Events::MaxLineCaptures.new(value: 12)
370
+ end
371
+
372
+ it 'interprets infinity' do
373
+ producer.record_max_line_captures Float::INFINITY
374
+ expect(consumer.call).to eq Events::MaxLineCaptures.new(value: Float::INFINITY)
375
+ end
376
+ end
377
+
378
+
379
+ describe 'exceptions' do
380
+ def record_exception(linenum=nil, &raises_exception)
381
+ raises_exception.call
382
+ rescue Exception
383
+ producer.record_exception linenum, $!
384
+ return raises_exception.source_location.last
385
+ end
386
+
387
+ def assert_exception(recorded_exception, options={})
388
+ expect(recorded_exception).to be_a_kind_of Events::Exception
389
+ expect(recorded_exception.line_number).to eq options[:recorded_line_no]
390
+ expect(recorded_exception.class_name ).to match options[:class_name_matcher] if options[:class_name_matcher]
391
+ expect(recorded_exception.message ).to match options[:message_matcher] if options[:message_matcher]
392
+
393
+ backtrace = recorded_exception.backtrace
394
+ expect(backtrace).to be_a_kind_of Array
395
+ expect(backtrace).to be_all { |frame| String === frame }
396
+ frame = backtrace[options[:backtrace_index]||0]
397
+ expect(frame).to match /(^|\b)#{options[:backtrace_filename]}(\b|$)/ if options[:backtrace_filename]
398
+ expect(frame).to match /(^|\b)#{options[:backtrace_line]}(\b|$)/ if options[:backtrace_line]
399
+ end
400
+
401
+ it 'emits the line_number, an escaped class_name, an escaped message, and escaped backtrace' do
402
+ backtrace_line = record_exception(12) { raise ZeroDivisionError, 'omg' }
403
+ assert_exception consumer.call,
404
+ recorded_line_no: 12,
405
+ class_name_matcher: /^ZeroDivisionError$/,
406
+ message_matcher: /\Aomg\Z/,
407
+ backtrace_index: 0,
408
+ backtrace_line: backtrace_line,
409
+ backtrace_filename: __FILE__
410
+ end
411
+
412
+ example 'Example: Common edge case: name error' do
413
+ backtrace_line = record_exception(99) { BasicObject.new.instance_eval { not_a_local_or_meth } }
414
+ backtrace_frame = 0
415
+ backtrace_frame += 1 if defined? Rubinius # their method missing goes into the kernel
416
+ assert_exception consumer.call,
417
+ recorded_line_no: 99,
418
+ class_name_matcher: /^NameError$/,
419
+ message_matcher: /\bnot_a_local_or_meth\b/,
420
+ backtrace_index: backtrace_frame,
421
+ backtrace_line: backtrace_line,
422
+ backtrace_filename: __FILE__
423
+ end
424
+
425
+ context 'when the exception is a SystemExit' do
426
+ it 'returns the status and does not record the exception' do
427
+ exception = nil
428
+ begin exit 22
429
+ rescue SystemExit
430
+ exception = $!
431
+ end
432
+
433
+ exitstatus = producer.record_exception(1, exception)
434
+ expect(exitstatus).to eq 22
435
+ finish!
436
+ expect(consumer.each.find { |e| e.kind_of? Events::Exception }).to eq nil
437
+ end
438
+ end
439
+
440
+ it 'works with objects whose boolean inquiries have been messed with (#131)' do
441
+ exception = begin; raise; rescue; $!; end
442
+ bad_bool = Object.new
443
+ def bad_bool.!(*) raise; end
444
+ producer.record_exception bad_bool, exception # should not explode
445
+ end
446
+
447
+ context 'recorded line number | line num is provided | it knows the file | exception comes from within file' do
448
+ let(:exception) { begin; raise "zomg"; rescue; $!; end }
449
+ let(:linenum) { __LINE__ - 1 }
450
+ example "provided one | true | true | true" do
451
+ producer.filename = __FILE__
452
+ producer.record_exception 12, exception
453
+ assert_exception consumer.call, recorded_line_no: 12
454
+ end
455
+ example "provided one | true | true | false" do
456
+ exception.backtrace.replace ['otherfile.rb']
457
+ producer.record_exception 12, exception
458
+ producer.filename = __FILE__
459
+ assert_exception consumer.call, recorded_line_no: 12
460
+ end
461
+ example "provided one | true | false | true" do
462
+ producer.filename = nil
463
+ producer.record_exception 12, exception
464
+ assert_exception consumer.call, recorded_line_no: 12
465
+ end
466
+ example "provided one | true | false | false" do
467
+ exception.backtrace.replace ['otherfile.rb']
468
+ producer.filename = nil
469
+ producer.record_exception 12, exception
470
+ assert_exception consumer.call, recorded_line_no: 12
471
+ end
472
+ example "from backtrace | false | true | true" do
473
+ producer.filename = __FILE__
474
+ producer.record_exception nil, exception
475
+ assert_exception consumer.call, recorded_line_no: linenum
476
+ end
477
+ example "-1 | false | true | false" do
478
+ exception.backtrace.replace ['otherfile.rb']
479
+ producer.filename = __FILE__
480
+ producer.record_exception nil, exception
481
+ assert_exception consumer.call, recorded_line_no: -1
482
+ end
483
+ example "-1 | false | false | true" do
484
+ producer.filename = nil
485
+ producer.record_exception nil, exception
486
+ assert_exception consumer.call, recorded_line_no: -1
487
+ end
488
+ example "-1 | false | false | false" do
489
+ exception.backtrace.replace ['otherfile.rb']
490
+ producer.filename = nil
491
+ producer.record_exception nil, exception
492
+ assert_exception consumer.call, recorded_line_no: -1
493
+ end
494
+ end
495
+ end
496
+
497
+ describe 'seeing is believing version' do
498
+ describe 'recording the version' do
499
+ it 'emits the version info' do
500
+ producer.record_sib_version '1.2.3'
501
+ expect(consumer.call).to eq Events::SiBVersion.new(value: "1.2.3")
502
+ end
503
+ end
504
+
505
+ specify 'version return the version, if it has been set' do
506
+ expect(producer.version).to eq nil
507
+ producer.record_sib_version '4.5.6'
508
+ expect(producer.version).to eq '4.5.6'
509
+ end
510
+ end
511
+
512
+ describe 'record_ruby_version' do
513
+ it 'emits the ruby version info' do
514
+ producer.record_ruby_version 'o.m.g.'
515
+ expect(consumer.call).to eq Events::RubyVersion.new(value: 'o.m.g.')
516
+ end
517
+ end
518
+
519
+ describe 'record_filename' do
520
+ it 'sets the filename' do
521
+ producer.record_filename 'this-iz-mah-file.rb'
522
+ expect(producer.filename).to eq 'this-iz-mah-file.rb'
523
+ end
524
+ it 'emits the filename' do
525
+ producer.record_filename 'this-iz-mah-file.rb'
526
+ expect(consumer.call).to eq Events::Filename.new(value: 'this-iz-mah-file.rb')
527
+ end
528
+ end
529
+
530
+ describe 'stdout' do
531
+ it 'is emitted along with the events from the event stream' do
532
+ stdout_producer.puts "this is the stdout¡"
533
+ expect(consumer.call).to eq Events::Stdout.new(value: "this is the stdout¡\n")
534
+ end
535
+ specify 'each line is emitted as an event' do
536
+ stdout_producer.puts "first"
537
+ stdout_producer.puts "second\nthird"
538
+ expect(consumer.call).to eq Events::Stdout.new(value: "first\n")
539
+ expect(consumer.call).to eq Events::Stdout.new(value: "second\n")
540
+ expect(consumer.call).to eq Events::Stdout.new(value: "third\n")
541
+ end
542
+ end
543
+
544
+ describe 'stderr' do
545
+ it 'is emitted along with the events from the event stream' do
546
+ stderr_producer.puts "this is the stderr¡"
547
+ expect(consumer.call).to eq Events::Stderr.new(value: "this is the stderr¡\n")
548
+ end
549
+ specify 'each line is emitted as an event' do
550
+ stderr_producer.puts "first"
551
+ stderr_producer.puts "second\nthird"
552
+ expect(consumer.call).to eq Events::Stderr.new(value: "first\n")
553
+ expect(consumer.call).to eq Events::Stderr.new(value: "second\n")
554
+ expect(consumer.call).to eq Events::Stderr.new(value: "third\n")
555
+ end
556
+ end
557
+
558
+ describe 'record_exec' do
559
+ it 'records the event and the inspection of the args that were given to exec' do
560
+ producer.record_exec(["ls", "-l"])
561
+ expect(consumer.call).to eq Events::Exec.new(args: '["ls", "-l"]')
562
+ end
563
+ end
564
+
565
+ describe 'record_num_lines' do
566
+ it 'interprets numbers' do
567
+ producer.record_num_lines 21
568
+ expect(consumer.call).to eq Events::NumLines.new(value: 21)
569
+ end
570
+ end
571
+
572
+ describe 'finish!' do
573
+ it 'stops the producer from producing' do
574
+ read, write = IO.pipe
575
+ producer = SeeingIsBelieving::EventStream::Producer.new write
576
+ producer.finish!
577
+ producer.record_filename("zomg")
578
+ write.close
579
+ expect(read.gets).to eq nil
580
+ end
581
+ end
582
+
583
+ describe 'final events' do
584
+ it 'emits a StdoutClosed event when consumer side of stdout closes' do
585
+ stdout_consumer.close
586
+ expect(consumer.call).to eq Events::StdoutClosed.new(side: :consumer)
587
+ end
588
+ it 'emits a StdoutClosed event when producer side of stdout closes' do
589
+ stdout_producer.close
590
+ expect(consumer.call).to eq Events::StdoutClosed.new(side: :producer)
591
+ end
592
+
593
+ it 'emits a StderrClosed event when consumer side of stderr closes' do
594
+ stderr_consumer.close
595
+ expect(consumer.call).to eq Events::StderrClosed.new(side: :consumer)
596
+ end
597
+ it 'emits a StderrClosed event when producer side of stderr closes' do
598
+ stderr_producer.close
599
+ expect(consumer.call).to eq Events::StderrClosed.new(side: :producer)
600
+ end
601
+
602
+ it 'emits a EventStreamClosed event when consumer side of event_stream closes' do
603
+ eventstream_consumer.close
604
+ expect(consumer.call).to eq Events::EventStreamClosed.new(side: :consumer)
605
+ end
606
+ it 'emits a EventStreamClosed event when producer side of event_stream closes' do
607
+ eventstream_producer.close
608
+ expect(consumer.call).to eq Events::EventStreamClosed.new(side: :producer)
609
+ end
610
+
611
+ it 'emits a Exitstatus event on process_exitstatus' do
612
+ consumer.process_exitstatus 92
613
+ expect(consumer.call).to eq Events::Exitstatus.new(value: 92)
614
+ end
615
+
616
+ it 'translates missing statusses to 1 (eg this happens on my machine when the program segfaults, see #100)' do
617
+ # I'm not totally sure this is the right thing for it to do, but a segfault is the only way
618
+ # I know of to invoke this situation, and a segfault is printable, so until I get some info
619
+ # that proves this is the wrong thing to do, we're just going to give it a normal exit status
620
+ # since that's the easiest thing to do, and it's more correct in this one case.
621
+ consumer.process_exitstatus nil
622
+ expect(consumer.call).to eq Events::Exitstatus.new(value: 1)
623
+ end
624
+
625
+ it 'emits a Finished event when all streams are closed and it has an exit status' do
626
+ consumer.process_exitstatus 1
627
+ close_streams eventstream_producer, stdout_producer, stderr_producer
628
+ expect(consumer.each.to_a.last).to eq Events::Finished.new
629
+ end
630
+
631
+ it 'emits a Timeout event on process_timeout' do
632
+ consumer.process_timeout 1.23
633
+ expect(consumer.call).to eq Events::Timeout.new(seconds:1.23)
634
+ end
635
+
636
+ it 'emits a Finished event when all streams are closed and it has a timeout' do
637
+ consumer.process_timeout 1
638
+ close_streams eventstream_producer, stdout_producer, stderr_producer
639
+ expect(consumer.each.to_a.last).to eq Events::Finished.new
640
+ end
641
+ end
642
+
643
+
644
+ describe Events do
645
+ specify 'Event raises an error if .event_name was not overridden' do
646
+ expect { Event.event_name }.to raise_error NotImplementedError
647
+ end
648
+ specify 'all events have a reasonable event name' do
649
+ pairs = [
650
+ [Events::Stdout , :stdout],
651
+ [Events::Stderr , :stderr],
652
+ [Events::MaxLineCaptures , :max_line_captures],
653
+ [Events::Filename , :filename],
654
+ [Events::NumLines , :num_lines],
655
+ [Events::SiBVersion , :sib_version],
656
+ [Events::RubyVersion , :ruby_version],
657
+ [Events::Exitstatus , :exitstatus],
658
+ [Events::Timeout , :timeout],
659
+ [Events::Exec , :exec],
660
+ [Events::ResultsTruncated , :results_truncated],
661
+ [Events::LineResult , :line_result],
662
+ [Events::Exception , :exception],
663
+ [Events::StdoutClosed , :stdout_closed],
664
+ [Events::StderrClosed , :stderr_closed],
665
+ [Events::EventStreamClosed, :event_stream_closed],
666
+ [Events::Finished , :finished],
667
+ [Events::FileLoaded , :file_loaded],
668
+ ]
669
+ pairs.each { |klass, name| expect(klass.event_name).to eq name }
670
+
671
+ events_we_tested = pairs.map(&:first).flatten
672
+ event_classes = Events.constants.map { |name| Events.const_get name }
673
+ expect(event_classes - events_we_tested).to eq []
674
+ end
675
+ specify 'their event_name and attributes are included in their as_json' do
676
+ expect(Events::Stdout.new(value: "abc").as_json).to eq [:stdout, {value: "abc"}]
677
+ end
678
+ specify 'MaxLineCaptures#as_json includes is_infinity, and sets value to -1 in this case' do
679
+ expect(Events::MaxLineCaptures.new(value: Float::INFINITY).as_json).to eq [:max_line_captures, {value: -1, is_infinity: true}]
680
+ expect(Events::MaxLineCaptures.new(value: 123).as_json).to eq [:max_line_captures, {value: 123, is_infinity: false}]
681
+ end
682
+ end
683
+
684
+ require 'seeing_is_believing/event_stream/handlers/stream_json_events'
685
+ describe Handlers::StreamJsonEvents do
686
+ it 'writes each event\'s json representation to the stream' do
687
+ stream = ""
688
+ handler = described_class.new stream
689
+
690
+ handler.call Events::Stdout.new(value: "abc")
691
+ expect(stream).to eq %'["stdout",{"value":"abc"}]\n'
692
+
693
+ handler.call Events::Finished.new
694
+ expect(stream).to eq %'["stdout",{"value":"abc"}]\n'+
695
+ %'["finished",{}]\n'
696
+ end
697
+
698
+ it 'calls flush after each event, when the stream responds to it' do
699
+ stream = object_spy $stdout
700
+ flushcount = 0
701
+ allow(stream).to receive(:flush) { flushcount += 1 }
702
+
703
+ handler = described_class.new stream
704
+ expect(flushcount).to eq 0
705
+
706
+ handler.call Events::Stdout.new(value: "abc")
707
+ expect(flushcount).to eq 1
708
+
709
+ handler.call Events::Finished.new
710
+ expect(flushcount).to eq 2
711
+ end
712
+ end
713
+
714
+ describe Handlers::Debug do
715
+ let(:stream) { "" }
716
+ let(:events_seen) { [] }
717
+ let(:debugger) { SeeingIsBelieving::Debugger.new stream: stream }
718
+ let(:parent_observer) { lambda { |event| events_seen << event } }
719
+ let(:debug_handler) { described_class.new(debugger, parent_observer) }
720
+
721
+ it 'passes events through to the parent observer' do
722
+ event = Events::Stdout.new(value: "zomg")
723
+ debug_handler.call(event)
724
+ expect(events_seen).to eq [event]
725
+ end
726
+
727
+ it 'generally prints things, prettily, wide and short' do
728
+ [ Events::Stdout.new(value: "short"),
729
+ Events::Stdout.new(value: "long"*1000),
730
+ Events::Exec.new(args: ["a", "b", "c"]),
731
+ Events::StdoutClosed.new(side: :consumer),
732
+ Events::Exception.new(line_number: 100,
733
+ class_name: "SomethingException",
734
+ message: "The things, they blew up!",
735
+ backtrace: ["a"*10,"b"*2000]),
736
+ Events::Finished.new,
737
+ ].each { |event| debug_handler.call event }
738
+
739
+ expect(stream).to match /^Stdout\b/ # the events al made it
740
+ expect(stream).to match /^Exec\b/
741
+ expect(stream).to match /^StdoutClosed\b/
742
+ expect(stream).to match /^Exception\b/
743
+ expect(stream).to match /^Finished\b/
744
+ expect(stream).to match /^\| - a+/ # a backtrace in there
745
+ expect(stream).to match /\.{3}$/ # truncation indication
746
+ stream.each_line do |line|
747
+ expect(line.length).to be <= 151 # long lines got truncated (151 b/c newline is counted)
748
+ end
749
+ end
750
+ end
751
+
752
+ # most tests are just in the sense that fkn everything uses it all over the place
753
+ # but they use the valid cases, so this is just hitting the invalid one
754
+ require 'seeing_is_believing/event_stream/handlers/update_result'
755
+ describe Handlers::UpdateResult do
756
+ it 'raises an error if it sees an event it doesn\'t know' do
757
+ expect { described_class.new(double :result).call("unknown event") }
758
+ .to raise_error /unknown event/
759
+ end
760
+ end
761
+ end
762
+ end