From ac8fe85810f00055d0be47c7d2a32515b798aa03 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Sun, 10 Aug 2014 22:09:29 +0000 Subject: [PATCH] Add output format representation Upcoming commits will add a progressive reporter that does NOT require a rewindable output. Usefull for imperfect terminal emulations as on CI etc. --- lib/mutant.rb | 5 +- lib/mutant/reporter.rb | 10 ++ lib/mutant/reporter/cli.rb | 123 ++++++++------------ lib/mutant/reporter/cli/format.rb | 157 ++++++++++++++++++++++++++ lib/mutant/reporter/cli/printer.rb | 2 + lib/mutant/reporter/cli/tput.rb | 32 ++++++ lib/mutant/reporter/null.rb | 12 ++ lib/mutant/reporter/trace.rb | 15 ++- lib/mutant/runner.rb | 3 + spec/unit/mutant/cli_spec.rb | 10 +- spec/unit/mutant/reporter/cli_spec.rb | 9 +- 11 files changed, 290 insertions(+), 88 deletions(-) create mode 100644 lib/mutant/reporter/cli/format.rb create mode 100644 lib/mutant/reporter/cli/tput.rb diff --git a/lib/mutant.rb b/lib/mutant.rb index 6cb62011..095dc319 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -17,6 +17,7 @@ require 'anima' require 'concord' require 'morpher' require 'parallel' +require 'open3' # Library namespace module Mutant @@ -190,6 +191,8 @@ require 'mutant/reporter/null' require 'mutant/reporter/trace' require 'mutant/reporter/cli' require 'mutant/reporter/cli/printer' +require 'mutant/reporter/cli/tput' +require 'mutant/reporter/cli/format' require 'mutant/zombifier' require 'mutant/zombifier/file' @@ -204,7 +207,7 @@ module Mutant includes: EMPTY_ARRAY, requires: EMPTY_ARRAY, isolation: Mutant::Isolation::Fork, - reporter: Reporter::CLI.new($stdout), + reporter: Reporter::CLI.build($stdout), zombie: false, processes: Parallel.processor_count, expected_coverage: 100.0 diff --git a/lib/mutant/reporter.rb b/lib/mutant/reporter.rb index 20f32e0c..ebc00cf5 100644 --- a/lib/mutant/reporter.rb +++ b/lib/mutant/reporter.rb @@ -13,6 +13,16 @@ module Mutant # abstract_method :warn + # Report start + # + # @param [Env] env + # + # @return [self] + # + # @api private + # + abstract_method :start + # Report collector state # # @param [Runner::Collector] collector diff --git a/lib/mutant/reporter/cli.rb b/lib/mutant/reporter/cli.rb index fe6d9be5..605bf0e1 100644 --- a/lib/mutant/reporter/cli.rb +++ b/lib/mutant/reporter/cli.rb @@ -2,47 +2,50 @@ module Mutant class Reporter # Reporter that reports in human readable format class CLI < self - include Concord.new(:output) + include Concord.new(:output, :format) - NL = "\n".freeze - CLEAR_PREV_LINE = "\e[1A\e[2K".freeze - - # Output abstraction to decouple tty? from buffer - class Output - include Concord.new(:tty, :buffer) - - # Test if output is a tty - # - # @return [Boolean] - # - # @api private - # - def tty? - @tty - end - - [:puts, :write].each do |name| - define_method(name) do |*args, &block| - buffer.public_send(name, *args, &block) - end - end - - end # Output - - # Rate per second progress report fires - OUTPUT_RATE = 1.0 / 20 - - # Initialize object + # Build reporter # - # @return [undefined] + # @param [IO] output + # + # @return [Reporter::CLI] # # @api private # - def initialize(*) - super - @last_frame = nil - @last_length = 0 - @tty = output.respond_to?(:tty?) && output.tty? + def self.build(output) + ci = ENV.key?('CI') + tty = output.respond_to?(:tty?) && output.tty? + format = Format::Framed.new( + tty: tty, + tput: Tput::INSTANCE, + ) + + # Upcoming commits implementing progressive format will change this to + # the equivalent of: + # + # if !ci && tty && Tput::INSTANCE.available + # Format::Framed.new( + # tty: tty, + # tput: Tput::INSTANCE, + # ) + # else + # Format::Progressive.new( + # tty: tty, + # ) + # end + + new(output, format) + end + + # Report start + # + # @param [Env] env + # + # @api private + # + def start(env) + write(format.start(env)) + self end # Report progress object @@ -54,8 +57,8 @@ module Mutant # @api private # def progress(collector) - throttle do - swap(frame(Printer::Collector, collector)) + format.throttle do + write(format.progress(collector)) end self @@ -83,27 +86,13 @@ module Mutant # @api private # def report(env) - swap(frame(Printer::EnvResult, env)) + write(format.report(env)) self end private - # Compute progress frame - # - # @return [String] - # - # @api private - # - def frame(reporter, object) - buffer = StringIO.new - buffer.write(clear_command) if @tty - reporter.run(Output.new(@tty, buffer), object) - buffer.rewind - buffer.read - end - - # Swap output frame + # Write output frame # # @param [String] frame # @@ -111,32 +100,8 @@ module Mutant # # @api private # - def swap(frame) + def write(frame) output.write(frame) - @last_length = frame.split(NL).length - end - - # Call block throttled - # - # @return [undefined] - # - # @api private - # - def throttle - now = Time.now - return if @last_frame && (now - @last_frame) < OUTPUT_RATE - yield - @last_frame = now - end - - # Return clear command for last frame length - # - # @return [String] - # - # @api private - # - def clear_command - CLEAR_PREV_LINE * @last_length end end # CLI diff --git a/lib/mutant/reporter/cli/format.rb b/lib/mutant/reporter/cli/format.rb new file mode 100644 index 00000000..8527a5d4 --- /dev/null +++ b/lib/mutant/reporter/cli/format.rb @@ -0,0 +1,157 @@ +module Mutant + class Reporter + class CLI + # CLI output format + class Format + include AbstractType, Anima.new(:tty) + + # Return progress representation + # + # @param [Runner::Collector] collector + # + # @return [String] + # + # @api private + # + abstract_method :progress + + # Throttle report execution + # + # @return [self] + # + # @api private + # + abstract_method :throttle + + # Format result + # + # @param [Result::Env] env + # + # @return [String] + # + # @api private + # + def report(env) + format(Printer::EnvResult, env) + end + + # Output abstraction to decouple tty? from buffer + class Output + include Concord.new(:tty, :buffer) + + # Test if output is a tty + # + # @return [Boolean] + # + # @api private + # + def tty? + @tty + end + + [:puts, :write].each do |name| + define_method(name) do |*args, &block| + buffer.public_send(name, *args, &block) + end + end + + end # Output + + private + + # Format object with printer + # + # @param [Class:Printer] printer + # @param [Object] object + # + # @return [String] + # + # @api private + # + def format(printer, object) + buffer = new_buffer + printer.run(Output.new(tty, buffer), object) + buffer.rewind + buffer.read + end + + # Format for framed rewindable output + class Framed < self + include anima.add(:tput) + + BUFFER_FLAGS = 'a+'.freeze + + # Rate per second progress report fires + OUTPUT_RATE = 1.0 / 20 + + # Initialize object + # + # @return [undefined] + # + # @api private + # + def initialize(*) + super + @last_frame = nil + end + + # Format start + # + # @param [Env] env + # + # @return [String] + # + # @api private + # + def start(_env) + tput.prepare + end + + # Format progress + # + # @param [Runner::Collector] collector + # + # @return [String] + # + # @api private + # + def progress(collector) + format(Printer::Collector, collector) + end + + # Call block throttled + # + # @return [self] + # + # @api private + # + def throttle + now = Time.now + return if @last_frame && (now - @last_frame) < OUTPUT_RATE + yield + @last_frame = now + self + end + + private + + # Return new buffer + # + # @return [StringIO] + # + # @api private + # + def new_buffer + # For some reason this raises an Ernno::EACCESS errror: + # + # StringIO.new(Tput::INSTANCE.restore, BUFFER_FLAGS) + # + buffer = StringIO.new + buffer << tput.restore + end + + end # Framed + end # Format + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/cli/printer.rb b/lib/mutant/reporter/cli/printer.rb index 0f36f948..050cfa01 100644 --- a/lib/mutant/reporter/cli/printer.rb +++ b/lib/mutant/reporter/cli/printer.rb @@ -5,6 +5,8 @@ module Mutant class Printer include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object) + NL = "\n".freeze + # Run printer on object to output # # @param [IO] output diff --git a/lib/mutant/reporter/cli/tput.rb b/lib/mutant/reporter/cli/tput.rb new file mode 100644 index 00000000..4f585c90 --- /dev/null +++ b/lib/mutant/reporter/cli/tput.rb @@ -0,0 +1,32 @@ +module Mutant + class Reporter + class CLI + # Interface to the optionally present tput binary + class Tput + include Adamantium, Concord::Public.new(:available, :prepare, :restore) + + private_class_method :new + + capture = lambda do |command| + stdout, _stderr, exitstatus = Open3.capture3(command) + stdout if exitstatus.success? + end + + reset = capture.('tput reset') + save = capture.('tput sc') if reset + restore = capture.('tput rc') if save + clean = capture.('tput ed') if restore + + UNAVAILABLE = new(false, nil, nil) + + INSTANCE = + if save && restore && clean + new(true, reset + save, restore + clean) + else + UNAVAILABLE + end + + end # TPUT + end # CLI + end # Reporter +end # Mutant diff --git a/lib/mutant/reporter/null.rb b/lib/mutant/reporter/null.rb index 7092adc8..b4373e1c 100644 --- a/lib/mutant/reporter/null.rb +++ b/lib/mutant/reporter/null.rb @@ -29,6 +29,18 @@ module Mutant self end + # Report start + # + # @param [Object] _object + # + # @return [self] + # + # @api private + # + def start(_object) + self + end + # Report progress on object # # @param [Object] _object diff --git a/lib/mutant/reporter/trace.rb b/lib/mutant/reporter/trace.rb index b7ee0e27..829edceb 100644 --- a/lib/mutant/reporter/trace.rb +++ b/lib/mutant/reporter/trace.rb @@ -2,7 +2,7 @@ module Mutant class Reporter # Reporter to trace report calls, used as a spec adapter class Trace - include Adamantium::Mutable, Anima.new(:progress_calls, :report_calls, :warn_calls) + include Adamantium::Mutable, Anima.new(:start_calls, :progress_calls, :report_calls, :warn_calls) # Return new trace reporter # @@ -40,6 +40,19 @@ module Mutant self end + # Report new progress on object + # + # @param [Object] object + # + # @return [self] + # + # @api private + # + def start(object) + start_calls << object + self + end + # Report new progress on object # # @param [Object] object diff --git a/lib/mutant/runner.rb b/lib/mutant/runner.rb index 91f98a9a..a1679e17 100644 --- a/lib/mutant/runner.rb +++ b/lib/mutant/runner.rb @@ -17,6 +17,9 @@ module Mutant @mutations = env.mutations.dup config.integration.setup + + config.reporter.start(env) + run @result = @collector.result.update(done: true) diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index acfa953b..02192891 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -65,18 +65,16 @@ RSpec.describe Mutant::CLI do subject { object.new(arguments) } # Defaults - let(:expected_filter) { Morpher.evaluator(s(:true)) } - let(:expected_integration) { Mutant::Integration::Null.new } - let(:expected_reporter) { Mutant::Reporter::CLI.new($stdout) } - let(:expected_matcher_config) { default_matcher_config } + let(:expected_filter) { Morpher.evaluator(s(:true)) } + let(:expected_integration) { Mutant::Integration::Null.new } + let(:expected_reporter) { Mutant::Config::DEFAULT.reporter } + let(:expected_matcher_config) { default_matcher_config } let(:default_matcher_config) do Mutant::Matcher::Config::DEFAULT .update(match_expressions: expressions.map(&Mutant::Expression.method(:parse))) end - let(:ns) { Mutant::Matcher } - let(:flags) { [] } let(:expressions) { %w[TestApp*] } diff --git a/spec/unit/mutant/reporter/cli_spec.rb b/spec/unit/mutant/reporter/cli_spec.rb index 0e46393f..cdcceef3 100644 --- a/spec/unit/mutant/reporter/cli_spec.rb +++ b/spec/unit/mutant/reporter/cli_spec.rb @@ -1,7 +1,14 @@ RSpec.describe Mutant::Reporter::CLI do - let(:object) { described_class.new(output) } + let(:object) { described_class.new(output, format) } let(:output) { StringIO.new } + let(:format) do + described_class::Format::Framed.new( + tty: false, + tput: described_class::Tput::UNAVAILABLE + ) + end + def contents output.rewind output.read