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.
This commit is contained in:
Markus Schirp 2014-08-10 22:09:29 +00:00
parent 3654ba010d
commit ac8fe85810
11 changed files with 290 additions and 88 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -17,6 +17,9 @@ module Mutant
@mutations = env.mutations.dup
config.integration.setup
config.reporter.start(env)
run
@result = @collector.result.update(done: true)

View file

@ -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*] }

View file

@ -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