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,195 @@
1
+ # Not sure what the best way to evaluate these is
2
+ # This approach will move the old file out of the way,
3
+ # write the program in its place, invoke it, and move it back.
4
+ #
5
+ # Another option is to replace __FILE__ macros ourselves
6
+ # and then write to a temp file but evaluate in the context
7
+ # of the expected directory. Some issues could arise with this,
8
+ # though: if you required the file again, it wouldn't already
9
+ # be in the loaded features (might be able to just add it)
10
+ # if you did something like File.read(__FILE__) it would
11
+ # read the wrong file... of course, since we rewrite the file,
12
+ # its body will be incorrect, anyway.
13
+
14
+ require 'rbconfig'
15
+ require 'socket'
16
+ require "childprocess"
17
+
18
+ require 'seeing_is_believing/result'
19
+ require 'seeing_is_believing/debugger'
20
+ require 'seeing_is_believing/swap_files'
21
+ require 'seeing_is_believing/event_stream/consumer'
22
+ require 'seeing_is_believing/event_stream/events'
23
+
24
+ # Forking locks up for some reason when we run SiB inside of SiB, so use `spawn`
25
+ ChildProcess.posix_spawn = true
26
+
27
+ # ChildProcess works on the M1 ("Apple Silicon"),
28
+ # but it emits a bunch of logs that wind up back in the editors.
29
+ # I opened an issue https://github.com/enkessler/childprocess/issues/176
30
+ #
31
+ # Nothing happened for 3 months, and I eventually switched it over to use `Kernel.spawn`
32
+ # But that only worked on my local machine, and failed in CI on Oses I don't have access to
33
+ # https://github.com/JoshCheek/seeing_is_believing/commit/f32929637625aece8ccb020c58470a5b4f659fbe
34
+ #
35
+ # Then Ryan patched it for me during a Seattle.rb meetup
36
+ # https://github.com/enkessler/childprocess/pull/177
37
+ #
38
+ # But their `arch` detection code gets back different values between Ruby 2.7.2 and 2.7.3:
39
+ # https://github.com/enkessler/childprocess/issues/179
40
+ #
41
+ # My gem has been broken on the M1 for... prob the entire life of the M1
42
+ # * https://github.com/JoshCheek/seeing_is_believing/issues/160
43
+ # * https://github.com/JoshCheek/seeing_is_believing/issues/161
44
+ #
45
+ # Sooooo... in order to unbreak it, I'm going to try to detect this situation
46
+ # and guerilla patch Childprocess so that it doesn't depend on `RbConfig::CONFIG['host_cpu']`
47
+ # we're essentially blowing away this method:
48
+ # https://github.com/enkessler/childprocess/blob/v4.1.0/lib/childprocess.rb#L131-L150
49
+ if RbConfig::CONFIG['host'] =~ /arm/ && RbConfig::CONFIG['host'] =~ /darwin/
50
+ def ChildProcess.arch
51
+ 'arm64'
52
+ end
53
+ end
54
+
55
+
56
+ class SeeingIsBelieving
57
+ class EvaluateByMovingFiles
58
+ def self.call(*args)
59
+ new(*args).call
60
+ end
61
+
62
+ attr_accessor :user_program, :rewritten_program, :provided_input, :require_flags, :load_path_flags, :encoding, :timeout_seconds, :debugger, :event_handler, :max_line_captures
63
+
64
+ attr_accessor :file_directory, :file_path, :local_cwd, :relative_filename, :backup_path
65
+
66
+ def initialize(file_path, user_program, rewritten_program, options={})
67
+ options = options.dup
68
+ self.user_program = user_program
69
+ self.rewritten_program = rewritten_program
70
+ self.encoding = options.delete(:encoding) || "u"
71
+ self.timeout_seconds = options.delete(:timeout_seconds) || 0 # 0 is the new infinity
72
+ self.provided_input = options.delete(:provided_input) || String.new
73
+ self.event_handler = options.delete(:event_handler) || raise(ArgumentError, "must provide an :event_handler")
74
+ self.load_path_flags = (options.delete(:load_path_dirs) || []).flat_map { |dir| ['-I', dir] }
75
+ self.require_flags = (options.delete(:require_files) || ['seeing_is_believing/the_matrix']).map { |filename| ['-r', filename] }.flatten
76
+ self.max_line_captures = (options.delete(:max_line_captures) || Float::INFINITY) # (optimization: child stops producing results at this number, even though it might make more sense for the consumer to stop emitting them)
77
+ self.local_cwd = options.delete(:local_cwd) || false
78
+ self.file_path = file_path
79
+ self.file_directory = File.dirname file_path
80
+ file_name = File.basename file_path
81
+ self.relative_filename = local_cwd ? file_name : file_path
82
+ self.backup_path = File.join file_directory, "seeing_is_believing_backup.#{file_name}"
83
+
84
+ options.any? && raise(ArgumentError, "Unknown options: #{options.inspect}")
85
+ end
86
+
87
+ def call
88
+ SwapFiles.call file_path, backup_path, user_program, rewritten_program do |swap_files|
89
+ evaluate_file swap_files
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def evaluate_file(swap_files)
96
+ event_server = TCPServer.new(0) # dynamically allocates an available port
97
+
98
+ # setup streams
99
+ stdout, child_stdout = IO.pipe("utf-8")
100
+ stderr, child_stderr = IO.pipe("utf-8")
101
+
102
+ # setup environment variables
103
+ env = ENV.to_hash.merge 'SIB_VARIABLES.MARSHAL.B64' =>
104
+ [Marshal.dump(
105
+ event_stream_port: event_server.addr[1],
106
+ max_line_captures: max_line_captures,
107
+ num_lines: user_program.lines.count,
108
+ filename: relative_filename,
109
+ )].pack('m0')
110
+
111
+ child = ChildProcess.build(*popen_args)
112
+ child.cwd = file_directory if local_cwd
113
+ child.leader = true
114
+ child.duplex = true
115
+ child.environment.merge!(env)
116
+ child.io.stdout = child_stdout
117
+ child.io.stderr = child_stderr
118
+
119
+ child.start
120
+
121
+ # close child streams b/c they won't emit EOF if parent still has an open reference
122
+ close_streams(child_stdout, child_stderr)
123
+ child.io.stdin.binmode
124
+ child.io.stdin.sync = true
125
+
126
+ # Start receiving events from the child
127
+ eventstream = event_server.accept
128
+
129
+ # send stdin (char at a time b/c input could come from a stream)
130
+ Thread.new do
131
+ begin
132
+ provided_input.each_char { |char| child.io.stdin.write char }
133
+ rescue
134
+ # don't explode if child closes IO
135
+ ensure
136
+ child.io.stdin.close
137
+ end
138
+ end
139
+
140
+ # set up the event consumer
141
+ consumer = EventStream::Consumer.new(events: eventstream, stdout: stdout, stderr: stderr)
142
+ consumer_thread = Thread.new do
143
+ consumer.each do |e|
144
+ swap_files.show_user_program if e.is_a? SeeingIsBelieving::EventStream::Events::FileLoaded
145
+ event_handler.call e
146
+ end
147
+ end
148
+
149
+ # wait for completion
150
+ if timeout_seconds == 0
151
+ child.wait
152
+ else
153
+ child.poll_for_exit(timeout_seconds)
154
+ end
155
+ consumer.process_exitstatus(child.exit_code)
156
+ consumer_thread.join
157
+ rescue ChildProcess::TimeoutError
158
+ consumer.process_timeout(timeout_seconds)
159
+ child.stop
160
+ consumer_thread.join
161
+ ensure
162
+ # On Windows, we need to call stop if there is an error since it interrupted
163
+ # the previos waiting/polling. If we don't call stop, in that situation, it will
164
+ # leave orphan processes. On Unix, we need to always call stop or it may leave orphans
165
+ begin
166
+ if ChildProcess.unix?
167
+ child.stop
168
+ elsif $!
169
+ child.stop
170
+ consumer.process_exitstatus(child.exit_code)
171
+ end
172
+ child.alive? && child.stop
173
+ rescue ChildProcess::Error
174
+ # On AppVeyor, I keep getting errors
175
+ # The handle is invalid: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/22
176
+ # Access is denied: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/24
177
+ end
178
+ close_streams(stdout, stderr, eventstream, event_server)
179
+ end
180
+
181
+ def popen_args
182
+ [RbConfig.ruby,
183
+ '-W0', # no warnings (b/c I hijack STDOUT/STDERR)
184
+ *(encoding ? ["-K#{encoding}"] : []), # allow the encoding to be set
185
+ '-I', File.realpath('..', __dir__), # add lib to the load path
186
+ *load_path_flags, # users can inject dirs to be added to the load path
187
+ *require_flags, # users can inject files to be required
188
+ relative_filename]
189
+ end
190
+
191
+ def close_streams(*streams)
192
+ streams.each { |io| io.close unless io.closed? }
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,221 @@
1
+ # encoding: utf-8
2
+
3
+ require 'seeing_is_believing/event_stream/events'
4
+ require 'seeing_is_believing/error'
5
+ require 'thread'
6
+
7
+ # Polyfill String#scrub on Ruby 2.0.0
8
+ require 'seeing_is_believing/compatibility'
9
+ using SeeingIsBelieving::Compatibility
10
+
11
+ class SeeingIsBelieving
12
+ module EventStream
13
+ class Consumer
14
+ # Contemplated doing FinishCriteria in binary, but the cost of doing it with an array
15
+ # like this is negligible and it has the nice advantage that the elements in the array
16
+ # are named # so if I ever look at it, I don't have to tranlsate a number to figure out
17
+ # the names https://gist.github.com/JoshCheek/10deb07277b6c85efc7b5e65c007785d
18
+ class FinishCriteria
19
+ EventThreadFinished = Module.new
20
+ StdoutThreadFinished = Module.new
21
+ StderrThreadFinished = Module.new
22
+ ProcessExited = Module.new
23
+
24
+ def initialize
25
+ @unmet_criteria = [
26
+ EventThreadFinished,
27
+ StdoutThreadFinished,
28
+ StderrThreadFinished,
29
+ ProcessExited,
30
+ ]
31
+ end
32
+
33
+ # finish criteria are satisfied,
34
+ # we can stop processing events
35
+ def satisfied?
36
+ @unmet_criteria.empty?
37
+ end
38
+
39
+ def event_thread_finished!
40
+ @unmet_criteria.delete EventThreadFinished
41
+ end
42
+
43
+ def stdout_thread_finished!
44
+ @unmet_criteria.delete StdoutThreadFinished
45
+ end
46
+
47
+ def stderr_thread_finished!
48
+ @unmet_criteria.delete StderrThreadFinished
49
+ end
50
+
51
+ def received_exitstatus!
52
+ @unmet_criteria.delete ProcessExited
53
+ end
54
+
55
+ def received_timeout!
56
+ @unmet_criteria.delete ProcessExited
57
+ end
58
+ end
59
+
60
+ # https://github.com/JoshCheek/seeing_is_believing/issues/46
61
+ def self.fix_encoding(str)
62
+ begin
63
+ str.encode! Encoding::UTF_8
64
+ rescue EncodingError
65
+ str = str.force_encoding(Encoding::UTF_8)
66
+ end
67
+ str.scrub('�')
68
+ end
69
+
70
+ def initialize(streams)
71
+ @finished = false
72
+ self.finish_criteria = FinishCriteria.new
73
+ self.queue = Queue.new
74
+ event_stream = streams.fetch :events
75
+ stdout_stream = streams.fetch :stdout
76
+ stderr_stream = streams.fetch :stderr
77
+ self.threads = [
78
+ Thread.new do
79
+ begin
80
+ stdout_stream.each_line { |line| queue << Events::Stdout.new(value: line) }
81
+ queue << Events::StdoutClosed.new(side: :producer)
82
+ rescue IOError
83
+ queue << Events::StdoutClosed.new(side: :consumer)
84
+ ensure
85
+ queue << lambda { finish_criteria.stdout_thread_finished! }
86
+ end
87
+ end,
88
+
89
+ Thread.new do
90
+ begin
91
+ stderr_stream.each_line { |line| queue << Events::Stderr.new(value: line) }
92
+ queue << Events::StderrClosed.new(side: :producer)
93
+ rescue IOError
94
+ queue << Events::StderrClosed.new(side: :consumer)
95
+ ensure
96
+ queue << lambda { finish_criteria.stderr_thread_finished! }
97
+ end
98
+ end,
99
+
100
+ Thread.new do
101
+ begin
102
+ event_stream.each_line { |line| queue << line }
103
+ queue << Events::EventStreamClosed.new(side: :producer)
104
+ rescue IOError
105
+ queue << Events::EventStreamClosed.new(side: :consumer)
106
+ ensure
107
+ queue << lambda { finish_criteria.event_thread_finished! }
108
+ end
109
+ end,
110
+ ]
111
+ end
112
+
113
+ def call(n=1)
114
+ return next_event if n == 1
115
+ Array.new(n) { next_event }
116
+ end
117
+
118
+ def each
119
+ return to_enum :each unless block_given?
120
+ yield call 1 until @finished
121
+ end
122
+
123
+ # NOTE: Note it's probably a bad plan to call these methods
124
+ # from within the same thread as the consumer, because if it
125
+ # blocks, who will remove items from the queue?
126
+ def process_exitstatus(status)
127
+ status ||= 1 # see #100
128
+ queue << lambda {
129
+ queue << Events::Exitstatus.new(value: status)
130
+ finish_criteria.received_exitstatus!
131
+ }
132
+ end
133
+ def process_timeout(seconds)
134
+ queue << lambda {
135
+ queue << Events::Timeout.new(seconds: seconds)
136
+ finish_criteria.received_timeout!
137
+ }
138
+ end
139
+
140
+ def join
141
+ threads.each(&:join)
142
+ end
143
+
144
+ private
145
+
146
+ attr_accessor :queue, :finish_criteria, :threads
147
+
148
+ def next_event
149
+ raise NoMoreEvents if @finished
150
+ case element = queue.shift
151
+ when String
152
+ event_for element
153
+ when Proc
154
+ element.call
155
+ finish_criteria.satisfied? &&
156
+ queue << Events::Finished.new
157
+ next_event
158
+ when Events::Finished
159
+ @finished = true
160
+ element
161
+ when Event
162
+ element
163
+ else
164
+ raise SeeingIsBelieving::UnknownEvent, "WAT IS THIS?: #{element.inspect}"
165
+ end
166
+ end
167
+
168
+ def shift_token(line)
169
+ event_name = line[/[^ ]+/]
170
+ line.sub!(/^\s*[^ ]+\s*/, '')
171
+ event_name
172
+ end
173
+
174
+ # For a consideration of many different ways of passing the message, see 5633064
175
+ def shift_string(line)
176
+ str = Marshal.load shift_token(line).unpack('m0').first
177
+ Consumer.fix_encoding(str)
178
+ end
179
+
180
+ def event_for(original_line)
181
+ line = original_line.chomp
182
+ event_name = shift_token(line).intern
183
+ case event_name
184
+ when :result
185
+ line_number = shift_token(line).to_i
186
+ type = shift_token(line).intern
187
+ inspected = shift_string(line)
188
+ Events::LineResult.new(type: type, line_number: line_number, inspected: inspected)
189
+ when :maxed_result
190
+ line_number = shift_token(line).to_i
191
+ type = shift_token(line).intern
192
+ Events::ResultsTruncated.new(type: type, line_number: line_number)
193
+ when :exception
194
+ Events::Exception.new \
195
+ line_number: shift_token(line).to_i,
196
+ class_name: shift_string(line),
197
+ message: shift_string(line),
198
+ backtrace: shift_token(line).to_i.times.map { shift_string line }
199
+ when :max_line_captures
200
+ token = shift_token(line)
201
+ value = token =~ /infinity/i ? Float::INFINITY : token.to_i
202
+ Events::MaxLineCaptures.new(value: value)
203
+ when :num_lines
204
+ Events::NumLines.new(value: shift_token(line).to_i)
205
+ when :sib_version
206
+ Events::SiBVersion.new(value: shift_string(line))
207
+ when :ruby_version
208
+ Events::RubyVersion.new(value: shift_string(line))
209
+ when :filename
210
+ Events::Filename.new(value: shift_string(line))
211
+ when :file_loaded
212
+ Events::FileLoaded.new
213
+ when :exec
214
+ Events::Exec.new(args: shift_string(line))
215
+ else
216
+ raise UnknownEvent, original_line.inspect
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,193 @@
1
+ require 'seeing_is_believing/hash_struct'
2
+
3
+ class SeeingIsBelieving
4
+ module EventStream
5
+ Event = HashStruct.anon do # one superclass to rule them all!
6
+ def self.event_name
7
+ raise NotImplementedError, "Subclass should have defined this!"
8
+ end
9
+
10
+ def event_name
11
+ self.class.event_name
12
+ end
13
+
14
+ def as_json
15
+ [event_name, to_h]
16
+ end
17
+ end
18
+
19
+ module Events
20
+ # A line was printed to stdout.
21
+ class Stdout < Event
22
+ def self.event_name
23
+ :stdout
24
+ end
25
+ attributes :value
26
+ end
27
+
28
+ # A line was printed to stderr.
29
+ class Stderr < Event
30
+ def self.event_name
31
+ :stderr
32
+ end
33
+ attributes :value
34
+ end
35
+
36
+ # The program will not record more results than this for a line.
37
+ # Note that if this is hit, it will emit an unrecorded_result.
38
+ class MaxLineCaptures < Event
39
+ def self.event_name
40
+ :max_line_captures
41
+ end
42
+ def as_json
43
+ value, is_infinity = if self.value == Float::INFINITY
44
+ [-1, true]
45
+ else
46
+ [self.value, false]
47
+ end
48
+ [event_name, {value: value, is_infinity: is_infinity}]
49
+ end
50
+ attribute :value
51
+ end
52
+
53
+ # Name of the file being evaluated.
54
+ class Filename < Event
55
+ def self.event_name
56
+ :filename
57
+ end
58
+ attributes :value
59
+ end
60
+
61
+ # For knowing when you can perform certain tasks (eg moving the file back)
62
+ class FileLoaded < Event
63
+ def self.event_name
64
+ :file_loaded
65
+ end
66
+ end
67
+
68
+ # Number of lines in the program.
69
+ class NumLines < Event
70
+ def self.event_name
71
+ :num_lines
72
+ end
73
+ attributes :value
74
+ end
75
+
76
+ # Version of SeeingIsBelieving used to evaluate the code.
77
+ # Equivalent to `SeeingIsBelieving::VERSION`, and `seeing_is_believing --version`
78
+ class SiBVersion < Event
79
+ def self.event_name
80
+ :sib_version
81
+ end
82
+ attributes :value
83
+ end
84
+
85
+ # Version of Ruby being used to evaluate the code.
86
+ # Equivalent to `RUBY_VERSION`
87
+ class RubyVersion < Event
88
+ def self.event_name
89
+ :ruby_version
90
+ end
91
+ attributes :value
92
+ end
93
+
94
+ # The process' exitstatus.
95
+ class Exitstatus < Event
96
+ def self.event_name
97
+ :exitstatus
98
+ end
99
+ attributes :value
100
+ end
101
+
102
+ # The process timed out
103
+ # note that you will probably not receive an exitstatus
104
+ # if this occurs. Though it's hypothetically possible...
105
+ # this is all asynchronous.
106
+ class Timeout < Event
107
+ def self.event_name
108
+ :timeout
109
+ end
110
+ attributes :seconds
111
+ end
112
+
113
+ # Emitted when the process invokes exec.
114
+ # Note that this could be a child process,
115
+ # so it does not necessarily mean there won't be any more line results
116
+ class Exec < Event
117
+ def self.event_name
118
+ :exec
119
+ end
120
+ attributes :args
121
+ end
122
+
123
+ # A line was executed, and its result recorded.
124
+ # Currently, type will either be :inspect, or :pp
125
+ # :pp is used by AnnotateMarkedLines to facilitate xmpfilter style.
126
+ # If you're consuming the event stream, it's safe to assume type will always be :inspect
127
+ # If you're using the library, it's whatever you've recorded it as (if you haven't changed this, it's :inspect)
128
+ class LineResult < Event
129
+ def self.event_name
130
+ :line_result
131
+ end
132
+ attributes :type, :line_number, :inspected
133
+ end
134
+
135
+ # There were more results than we are emitting for this line / type of recording
136
+ # See LineResult for explanation of types
137
+ # This would occur because the line was executed more times than the max.
138
+ class ResultsTruncated < Event
139
+ def self.event_name
140
+ :results_truncated
141
+ end
142
+ attributes :type, :line_number
143
+ end
144
+
145
+ # The program raised an exception and did not catch it.
146
+ # Note that currently `ExitStatus` exceptions are not emitted.
147
+ # That could change at some point as it seems like the stream consumer
148
+ # should decide whether they care about that rather than the producer.
149
+ class Exception < Event
150
+ def self.event_name
151
+ :exception
152
+ end
153
+ attributes :line_number, :class_name, :message, :backtrace
154
+ end
155
+
156
+ # The process's stdout stream was closed, there will be no more Stdout events.
157
+ # "side" will either be :producer or :consumer
158
+ class StdoutClosed < Event
159
+ def self.event_name
160
+ :stdout_closed
161
+ end
162
+ attributes :side
163
+ end
164
+
165
+ # The process's stderr stream was closed, there will be no more Stderr events.
166
+ # "side" will either be :producer or :consumer
167
+ class StderrClosed < Event
168
+ def self.event_name
169
+ :stderr_closed
170
+ end
171
+ attributes :side
172
+ end
173
+
174
+ # The process's event stream was closed, there will be no more events that come via the stream.
175
+ # Currently, that's all events except Stdout, StdoutClosed, Stderr, StdoutClosed, ExitStatus, and Finished
176
+ class EventStreamClosed < Event
177
+ def self.event_name
178
+ :event_stream_closed
179
+ end
180
+ attributes :side
181
+ end
182
+
183
+ # All streams are closed and the exit status is known.
184
+ # There will be no more events.
185
+ class Finished < Event
186
+ def self.event_name
187
+ :finished
188
+ end
189
+ attributes []
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,61 @@
1
+ class SeeingIsBelieving
2
+ module EventStream
3
+ module Handlers
4
+ # Even though the debugger can be disabled, which would push the decision of
5
+ # whether to report or not into the debugger where it belongs, you should still
6
+ # avoid using this class if you don't need it since it is expensive and there
7
+ # could be tens of millions of events, eg https://github.com/JoshCheek/seeing_is_believing/issues/12
8
+ class Debug
9
+ def initialize(debugger, handler)
10
+ @debugger = debugger
11
+ @handler = handler
12
+ @seen = ""
13
+ @line_width = 150 # debugger is basically for me, so giving it a nice wide width
14
+ @name_width = 20
15
+ @attr_width = @line_width - @name_width
16
+ end
17
+
18
+ def call(event)
19
+ observe event
20
+ finish if event.kind_of? Events::Finished
21
+ @handler.call event
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :debugger, :handler
27
+
28
+ def finish
29
+ @debugger.context("EVENTS") { @seen }
30
+ end
31
+
32
+ def observe(event)
33
+ name = event.class.name.split("::").last
34
+ lines = event.to_h
35
+ .map { |attribute, value|
36
+ case attribute
37
+ when :side then "#{attribute}: #{value}"
38
+ when :value then value.to_s.chomp
39
+ when :backtrace then indented = value.map { |v| "- #{v}" }
40
+ ["backtrace:", *indented]
41
+ else "#{attribute}: #{value.inspect}"
42
+ end
43
+ }
44
+ .flatten
45
+ joined = lines.join ", "
46
+ if joined.size < @attr_width
47
+ @seen << sprintf("%-#{@name_width}s%s\n", name, joined)
48
+ elsif lines.size == 1
49
+ @seen << sprintf("%-#{@name_width}s%s...\n", name, lines.first[0...@attr_width-3])
50
+ else
51
+ @seen << "#{name}\n"
52
+ lines.each { |line|
53
+ line = line[0...@line_width-5] << "..." if @line_width < line.length + 2
54
+ @seen << sprintf("| %s\n", line)
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ require 'seeing_is_believing/event_stream/events'
2
+
3
+ class SeeingIsBelieving
4
+ module EventStream
5
+ module Handlers
6
+ class RecordExitEvents
7
+ attr_reader :exitstatus
8
+ attr_reader :timeout_seconds
9
+
10
+ def initialize(next_handler)
11
+ @next_handler = next_handler
12
+ end
13
+
14
+ def call(event)
15
+ case event
16
+ when Events::Exitstatus
17
+ @exitstatus = event.value
18
+ when Events::Timeout
19
+ @timeout_seconds = event.seconds
20
+ end
21
+ @next_handler.call(event)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end