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:
parent
3654ba010d
commit
ac8fe85810
11 changed files with 290 additions and 88 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
# Build reporter
|
||||
#
|
||||
# @return [Boolean]
|
||||
# @param [IO] output
|
||||
#
|
||||
# @return [Reporter::CLI]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def tty?
|
||||
@tty
|
||||
end
|
||||
def self.build(output)
|
||||
ci = ENV.key?('CI')
|
||||
tty = output.respond_to?(:tty?) && output.tty?
|
||||
format = Format::Framed.new(
|
||||
tty: tty,
|
||||
tput: Tput::INSTANCE,
|
||||
)
|
||||
|
||||
[: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
|
||||
# Upcoming commits implementing progressive format will change this to
|
||||
# the equivalent of:
|
||||
#
|
||||
# @return [undefined]
|
||||
# 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 initialize(*)
|
||||
super
|
||||
@last_frame = nil
|
||||
@last_length = 0
|
||||
@tty = output.respond_to?(:tty?) && output.tty?
|
||||
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
|
||||
|
|
157
lib/mutant/reporter/cli/format.rb
Normal file
157
lib/mutant/reporter/cli/format.rb
Normal 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
|
|
@ -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
|
||||
|
|
32
lib/mutant/reporter/cli/tput.rb
Normal file
32
lib/mutant/reporter/cli/tput.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,9 @@ module Mutant
|
|||
@mutations = env.mutations.dup
|
||||
|
||||
config.integration.setup
|
||||
|
||||
config.reporter.start(env)
|
||||
|
||||
run
|
||||
|
||||
@result = @collector.result.update(done: true)
|
||||
|
|
|
@ -67,7 +67,7 @@ RSpec.describe Mutant::CLI do
|
|||
# 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_reporter) { Mutant::Config::DEFAULT.reporter }
|
||||
let(:expected_matcher_config) { default_matcher_config }
|
||||
|
||||
let(:default_matcher_config) do
|
||||
|
@ -75,8 +75,6 @@ RSpec.describe Mutant::CLI do
|
|||
.update(match_expressions: expressions.map(&Mutant::Expression.method(:parse)))
|
||||
end
|
||||
|
||||
let(:ns) { Mutant::Matcher }
|
||||
|
||||
let(:flags) { [] }
|
||||
let(:expressions) { %w[TestApp*] }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue