From 6893b819d3b900fbdb589db7df6d4fb3982cc981 Mon Sep 17 00:00:00 2001 From: Josh Cheek Date: Fri, 16 Jan 2015 04:01:13 -0700 Subject: [PATCH] Add --stream flag to print the event stream as it is seen * You may now provide an `event_handler` to toplevel * `SeeingIsBelieving`, it is expected to respond to `#call` and `#return_value` `SeeingIsBelieving.call` will return the handler's `return_value`. This is to avoid breaking the toplevel interface, as the default handler aggregates the events into a result, and returns that, allowing the return value to still be a Result, as before. * Events have an `#event_name` method, which returns their class's `event_name` * Handlers no longer do this silly `#to_proc` thing, they only need a `#call` method. * `EmitJsonEventsHandler` records `exitstatus` and `has_exception?` to * facilitate the needs of `Binary` without having to stack an `UpdateResultHandler` in there. That would be mildly annoying, and potentially expensive (one reason you might stream is to avoid building up a big in-memory object) * The recorded result is no longer part of the debugging output as there is not currently an interface to provide the debugger to an event stream. --- features/flags.feature | 30 ++++++++++++++-- lib/seeing_is_believing.rb | 14 ++++---- lib/seeing_is_believing/binary.rb | 8 ++--- lib/seeing_is_believing/binary/config.rb | 35 ++++++++++++------ .../event_stream/debugging_handler.rb | 4 +++ .../event_stream/emit_json_events_handler.rb | 36 ++++++++++++++++--- .../event_stream/events.rb | 4 +++ .../event_stream/update_result_handler.rb | 12 ++++--- spec/binary/config_spec.rb | 35 +++++++++++++++++- 9 files changed, 145 insertions(+), 33 deletions(-) diff --git a/features/flags.feature b/features/flags.feature index 4777025..a0d031f 100644 --- a/features/flags.feature +++ b/features/flags.feature @@ -436,8 +436,7 @@ Feature: Using flags And the exit status is 0 And stderr includes "REWRITTEN PROGRAM:" And stderr includes "$SiB" - And stderr includes "RESULT:" - And stderr includes "@results=" + And stderr includes "EVENTS:" And stderr includes "OUTPUT:" And stderr includes: """ @@ -481,3 +480,30 @@ Feature: Using flags } """ + Scenario: --stream prints events from the event stream as they are seen + Given the file "record_event_stream.rb" "3.times { |i| p i }" + When I run "seeing_is_believing record_event_stream.rb --stream" + Then stderr is empty + And the exit status is 0 + And stdout is: + """ + {"event":"stdout","value":"0\n"} + {"event":"stdout","value":"1\n"} + {"event":"stdout","value":"2\n"} + {"event":"ruby_version","value":"2.1.1"} + {"event":"sib_version","value":"3.0.0.beta.4"} + {"event":"filename","value":"record_event_stream.rb"} + {"event":"max_line_captures","value":Infinity} + {"event":"num_lines","value":1} + {"event":"line_result","type":"inspect","line_number":1,"inspected":"3"} + {"event":"event_stream_closed","side":"producer"} + {"event":"stdout_closed","side":"producer"} + {"event":"stderr_closed","side":"producer"} + {"event":"exitstatus","value":0} + {"event":"finished"} + """ + + Scenario: --stream respects the exit status + When I run "seeing_is_believing -ie 'exit 12' --stream" + Then stderr is empty + And the exit status is 12 diff --git a/lib/seeing_is_believing.rb b/lib/seeing_is_believing.rb index ef152be..9c8ee2c 100644 --- a/lib/seeing_is_believing.rb +++ b/lib/seeing_is_believing.rb @@ -11,6 +11,7 @@ class SeeingIsBelieving class Options < HashStruct + predicate(:event_handler) { EventStream::UpdateResultHandler.new Result.new } attribute(:filename) { nil } attribute(:encoding) { nil } attribute(:stdin) { "" } @@ -40,11 +41,12 @@ def call options.filename, options.max_line_captures - options.debugger.context("REWRITTEN PROGRAM") { new_program } + event_handler = options.event_handler + if options.debugger.enabled? + options.debugger.context("REWRITTEN PROGRAM") { new_program } + event_handler = EventStream::DebuggingHandler.new options.debugger, event_handler + end - result = Result.new - event_handler = EventStream::UpdateResultHandler.new(result) - event_handler = EventStream::DebuggingHandler.new(options.debugger, event_handler) EvaluateByMovingFiles.call \ new_program, options.filename, @@ -55,9 +57,7 @@ def call encoding: options.encoding, timeout_seconds: options.timeout_seconds - options.debugger.context("RESULT") { result.inspect } - - result + event_handler.return_value } end end diff --git a/lib/seeing_is_believing/binary.rb b/lib/seeing_is_believing/binary.rb index 7bca25b..e59651c 100644 --- a/lib/seeing_is_believing/binary.rb +++ b/lib/seeing_is_believing/binary.rb @@ -9,7 +9,7 @@ module Binary NONDISPLAYABLE_ERROR_STATUS = 2 # e.g. SiB was invoked incorrectly def self.call(argv, stdin, stdout, stderr) - config = Config.new.parse_args(argv, stderr).finalize(stdin, File) + config = Config.new.parse_args(argv, stderr).finalize(stdin, stdout, File) # TODO: move debugger initialization to finalize, so that parsing only takes argv engine = Engine.new config if config.print_help? @@ -48,9 +48,9 @@ def self.call(argv, stdin, stdout, stderr) require 'json' stdout.puts JSON.dump(engine.results.as_json) return SUCCESS_STATUS - end - - if config.debug? + elsif config.print_event_stream? + # no op, the event stream handler has been printing it all along + elsif config.debug? config.debugger.context("OUTPUT") { engine.annotated_body } else stdout.print engine.annotated_body diff --git a/lib/seeing_is_believing/binary/config.rb b/lib/seeing_is_believing/binary/config.rb index 0a6dd34..c02abab 100644 --- a/lib/seeing_is_believing/binary/config.rb +++ b/lib/seeing_is_believing/binary/config.rb @@ -24,6 +24,7 @@ def to_s predicate(:print_version) { false } predicate(:print_cleaned) { false } predicate(:print_help) { false } + predicate(:print_event_stream) { false } predicate(:result_as_json) { false } predicate(:inherit_exitstatus) { false } predicate(:debug) { false } @@ -101,6 +102,9 @@ def parse_args(args, debug_stream) when '-j', '--json' self.result_as_json = true + when '--stream' + self.print_event_stream = true + when '-h', '--help' self.print_help = true self.help_screen = Binary.help_screen(markers) @@ -170,14 +174,6 @@ def parse_args(args, debug_stream) end end - when /\A-K(.+)/ - self.lib_options.encoding = $1 - - when '-K', '--encoding' - next_arg.call arg, "an encoding" do |encoding| - self.lib_options.encoding = encoding - end - when '--shebang' executable = args.shift if executable @@ -187,6 +183,14 @@ def parse_args(args, debug_stream) saw_deprecated.call "SiB now uses the Ruby it was invoked with", arg end + when /\A-K(.+)/ + self.lib_options.encoding = $1 + + when '-K', '--encoding' + next_arg.call arg, "an encoding" do |encoding| + self.lib_options.encoding = encoding + end + when /^(-.|--.*)$/ add_error("#{arg} is not an option, see the help screen (-h) for a list of options") @@ -199,11 +203,14 @@ def parse_args(args, debug_stream) end filenames.size > 1 && - add_error("Can only have one filename but found #{filenames.map(&:inspect).join ', '}") + add_error("can only have one filename but found #{filenames.map(&:inspect).join ', '}") - result_as_json && annotator == AnnotateMarkedLines && + result_as_json? && annotator == AnnotateMarkedLines && add_error("SiB does not currently support output with both json and xmpfilter... maybe v4 :)") + print_event_stream? && (result_as_json? || annotator == AnnotateMarkedLines) && + add_error("can only have one output format, --stream is not compatible with --json, -x, and --xmpfilter-style") + self.filename = filenames.first self.lib_options.filename = as || filename self.lib_options.rewrite_code = annotator.expression_wrapper(markers) @@ -213,7 +220,7 @@ def parse_args(args, debug_stream) self end - def finalize(stdin, file_class) + def finalize(stdin, stdout, file_class) if filename && body add_error("Cannot give a program body and a filename to get the program body from.") elsif filename && file_class.exist?(filename) @@ -228,6 +235,12 @@ def finalize(stdin, file_class) else self.body = stdin.read end + + if print_event_stream? + require 'seeing_is_believing/event_stream/emit_json_events_handler' + lib_options.event_handler = EventStream::EmitJsonEventsHandler.new(stdout) + end + self end diff --git a/lib/seeing_is_believing/event_stream/debugging_handler.rb b/lib/seeing_is_believing/event_stream/debugging_handler.rb index 17e03a7..27a402b 100644 --- a/lib/seeing_is_believing/event_stream/debugging_handler.rb +++ b/lib/seeing_is_believing/event_stream/debugging_handler.rb @@ -20,6 +20,10 @@ def ==(other) other.kind_of?(self.class) && other.handler == handler && other.debugger == debugger end + def return_value + @handler.return_value + end + protected attr_reader :debugger, :handler diff --git a/lib/seeing_is_believing/event_stream/emit_json_events_handler.rb b/lib/seeing_is_believing/event_stream/emit_json_events_handler.rb index 9db6041..4822269 100644 --- a/lib/seeing_is_believing/event_stream/emit_json_events_handler.rb +++ b/lib/seeing_is_believing/event_stream/emit_json_events_handler.rb @@ -5,18 +5,46 @@ class EmitJsonEventsHandler attr_reader :stream def initialize(stream) - @flush = true if stream.respond_to? :flush - @stream = stream + @flush = true if stream.respond_to? :flush + @stream = stream + @has_exception = false + @exitstatus = :not_yet_seen end def call(event) + write_event event + record_outcome event + end + + def return_value + self + end + + def has_exception? + true + end + + def exitstatus + @exitstatus + end + + def ==(other) + other.kind_of?(self.class) && other.stream == stream + end + + private + + def write_event(event) @stream << JSON.dump(event.as_json) @stream << "\n" @stream.flush if @flush end - def ==(other) - other.kind_of?(self.class) && other.stream == stream + def record_outcome(event) + case event + when Events::Exception then @has_exception = true + when Events::Exitstatus then @exitstatus = event.value + end end end end diff --git a/lib/seeing_is_believing/event_stream/events.rb b/lib/seeing_is_believing/event_stream/events.rb index 5389d15..f25ff4a 100644 --- a/lib/seeing_is_believing/event_stream/events.rb +++ b/lib/seeing_is_believing/event_stream/events.rb @@ -7,6 +7,10 @@ def self.event_name raise NotImplementedError, "Subclass should have defined this!" end + def event_name + self.class.event_name + end + def as_json {event: self.class.event_name}.merge(to_h) end diff --git a/lib/seeing_is_believing/event_stream/update_result_handler.rb b/lib/seeing_is_believing/event_stream/update_result_handler.rb index 5609166..f1bcbf4 100644 --- a/lib/seeing_is_believing/event_stream/update_result_handler.rb +++ b/lib/seeing_is_believing/event_stream/update_result_handler.rb @@ -11,10 +11,6 @@ def initialize(result) @result = result end - def ==(other) - other.kind_of?(self.class) # this is dumb, it's b/c Result doesn't correctly implement == - end - def call(event) case event when LineResult then result.record_result(event.type, event.line_number, event.inspected) @@ -37,6 +33,14 @@ def call(event) else raise "Unknown event: #{event.inspect}" end end + + def ==(other) + other.kind_of?(self.class) # this is dumb, it's b/c Result doesn't correctly implement == + end + + def return_value + result + end end end end diff --git a/spec/binary/config_spec.rb b/spec/binary/config_spec.rb index 8e977d6..e378db2 100644 --- a/spec/binary/config_spec.rb +++ b/spec/binary/config_spec.rb @@ -511,10 +511,31 @@ def assert_deprecated(flag, *args) end end + describe 'print_event_stream?' do + it 'print_event_stream? is false by default' do + expect(parse([]).print_event_stream?).to eq false + end + it 'print_event_stream? can be turned on with --stream' do + expect(parse(['--stream']).print_event_stream?).to eq true + end + it 'adds an error if --stream is used with --json' do + expect(parse(['--stream'])).to_not have_error '--stream' + expect(parse(['--stream', '--json'])).to have_error '--stream' + expect(parse(['--json', '--stream'])).to have_error '--stream' + end + it 'adds an error if --stream is used with -x or --xmpfilter-style' do + expect(parse(['--stream'])).to_not have_error '--stream' + expect(parse(['--stream', '-x'])).to have_error '--stream' + expect(parse(['-x', '--stream'])).to have_error '--stream' + expect(parse(['--xmpfilter-style', '--stream'])).to have_error '--stream' + end + end + describe '.finalize' do let(:stdin_data) { 'stdin data' } let(:stdin) { object_double $stdin, read: stdin_data } + let(:stdout) { object_double $stdout } let(:file_class) { class_double File } let(:nonexisting_filename) { 'badfilename' } @@ -528,7 +549,7 @@ def assert_deprecated(flag, *args) end def call(attrs={}) - described_class.new(attrs).finalize(stdin, file_class) + described_class.new(attrs).finalize(stdin, stdout, file_class) end describe 'additional errors' do @@ -602,5 +623,17 @@ def call(attrs={}) expect(call(filename: nonexisting_filename).lib_options.stdin).to eq default end end + + describe 'lib_options.event_handler' do + it 'is an UpdateResultHandler when print_event_stream? is false' do + expect(call(print_event_stream: false).lib_options.event_handler) + .to be_an_instance_of SeeingIsBelieving::EventStream::UpdateResultHandler + end + it 'is an EmitJsonEventsHandler to stdout when print_event_stream? is true' do + handler = call(print_event_stream: true).lib_options.event_handler + expect(handler).to be_an_instance_of SeeingIsBelieving::EventStream::EmitJsonEventsHandler + expect(handler.stream).to eq stdout + end + end end end