andyw8-seeing_is_believing 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +60 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +2 -0
- data/README.md +70 -0
- data/Rakefile +88 -0
- data/appveyor.yml +32 -0
- data/bin/seeing_is_believing +7 -0
- data/docs/example.gif +0 -0
- data/docs/frog-brown.png +0 -0
- data/docs/sib-streaming.gif +0 -0
- data/features/deprecated-flags.feature +91 -0
- data/features/errors.feature +155 -0
- data/features/examples.feature +423 -0
- data/features/flags.feature +852 -0
- data/features/regression.feature +898 -0
- data/features/support/env.rb +102 -0
- data/features/xmpfilter-style.feature +471 -0
- data/lib/seeing_is_believing/binary/align_chunk.rb +47 -0
- data/lib/seeing_is_believing/binary/align_file.rb +24 -0
- data/lib/seeing_is_believing/binary/align_line.rb +25 -0
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +56 -0
- data/lib/seeing_is_believing/binary/annotate_every_line.rb +52 -0
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +179 -0
- data/lib/seeing_is_believing/binary/comment_lines.rb +36 -0
- data/lib/seeing_is_believing/binary/commentable_lines.rb +126 -0
- data/lib/seeing_is_believing/binary/config.rb +455 -0
- data/lib/seeing_is_believing/binary/data_structures.rb +58 -0
- data/lib/seeing_is_believing/binary/engine.rb +161 -0
- data/lib/seeing_is_believing/binary/format_comment.rb +79 -0
- data/lib/seeing_is_believing/binary/interline_align.rb +57 -0
- data/lib/seeing_is_believing/binary/remove_annotations.rb +113 -0
- data/lib/seeing_is_believing/binary/rewrite_comments.rb +62 -0
- data/lib/seeing_is_believing/binary.rb +73 -0
- data/lib/seeing_is_believing/code.rb +139 -0
- data/lib/seeing_is_believing/compatibility.rb +28 -0
- data/lib/seeing_is_believing/debugger.rb +32 -0
- data/lib/seeing_is_believing/error.rb +17 -0
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +195 -0
- data/lib/seeing_is_believing/event_stream/consumer.rb +221 -0
- data/lib/seeing_is_believing/event_stream/events.rb +193 -0
- data/lib/seeing_is_believing/event_stream/handlers/debug.rb +61 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
- data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +23 -0
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +41 -0
- data/lib/seeing_is_believing/event_stream/producer.rb +178 -0
- data/lib/seeing_is_believing/hard_core_ensure.rb +58 -0
- data/lib/seeing_is_believing/hash_struct.rb +206 -0
- data/lib/seeing_is_believing/result.rb +89 -0
- data/lib/seeing_is_believing/safe.rb +112 -0
- data/lib/seeing_is_believing/swap_files.rb +90 -0
- data/lib/seeing_is_believing/the_matrix.rb +97 -0
- data/lib/seeing_is_believing/version.rb +3 -0
- data/lib/seeing_is_believing/wrap_expressions.rb +265 -0
- data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +19 -0
- data/lib/seeing_is_believing.rb +69 -0
- data/seeing_is_believing.gemspec +84 -0
- data/spec/binary/alignment_specs.rb +27 -0
- data/spec/binary/comment_lines_spec.rb +852 -0
- data/spec/binary/config_spec.rb +831 -0
- data/spec/binary/engine_spec.rb +114 -0
- data/spec/binary/format_comment_spec.rb +210 -0
- data/spec/binary/marker_spec.rb +71 -0
- data/spec/binary/remove_annotations_spec.rb +342 -0
- data/spec/binary/rewrite_comments_spec.rb +106 -0
- data/spec/code_spec.rb +233 -0
- data/spec/debugger_spec.rb +45 -0
- data/spec/evaluate_by_moving_files_spec.rb +204 -0
- data/spec/event_stream_spec.rb +762 -0
- data/spec/hard_core_ensure_spec.rb +120 -0
- data/spec/hash_struct_spec.rb +514 -0
- data/spec/seeing_is_believing_spec.rb +1094 -0
- data/spec/sib_spec_helpers/version.rb +17 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/spec_helper_spec.rb +16 -0
- data/spec/wrap_expressions_spec.rb +1013 -0
- 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
|