free_mutant/lib/mutant/reporter/cli/printer.rb
2014-11-30 23:11:01 +00:00

556 lines
13 KiB
Ruby

module Mutant
class Reporter
class CLI
# CLI runner status printer base class
class Printer
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
delegate(:success?)
NL = "\n".freeze
# Run printer on object to output
#
# @param [IO] output
# @param [Object] object
#
# @return [self]
#
# @api private
#
def self.run(output, object)
new(output, object).run
end
# Run printer
#
# @return [self]
#
# @api private
#
abstract_method :run
private
# Return status color
#
# @return [Color]
#
# @api private
#
def status_color
success? ? Color::GREEN : Color::RED
end
# Visit a collection of objects
#
# @return [Class::Printer] printer
# @return [Enumerable<Object>] collection
#
# @return [undefined]
#
# @api private
#
def visit_collection(printer, collection)
collection.each do |object|
visit(printer, object)
end
end
# Visit object
#
# @param [Class::Printer] printer
# @param [Object] object
#
# @return [undefined]
#
# @api private
#
def visit(printer, object)
printer.run(output, object)
end
# Print an info line to output
#
# @return [undefined]
#
# @api private
#
def info(string, *arguments)
puts(format(string, *arguments))
end
# Print a status line to output
#
# @return [undefined]
#
# @api private
#
def status(string, *arguments)
puts(colorize(status_color, format(string, *arguments)))
end
# Print a line to output
#
# @return [undefined]
#
# @api private
#
def puts(string)
output.puts(string)
end
# Colorize message
#
# @param [Color] color
# @param [String] message
#
# @api private
#
# @return [String]
# if color is enabled
# unmodified message otherwise
#
def colorize(color, message)
color = Color::NONE unless tty?
color.format(message)
end
# Test if output is a tty
#
# @return [Boolean]
#
# @api private
#
def tty?
output.tty?
end
# Test if output can be colored
#
# @return [Boolean]
#
# @api private
#
alias_method :color?, :tty?
# Printer for runner status
class Status < self
delegate(:active_jobs, :env_result)
# Print progress for collector
#
# @return [self]
#
# @api private
#
def run
visit(EnvProgress, object.env_result)
info('Active subjects: %d', active_subject_results.length)
visit_collection(SubjectProgress, active_subject_results)
job_status
self
end
private
# Print worker status
#
# @return [undefined]
#
def job_status
return if active_jobs.empty?
info('Active Jobs:')
object.active_jobs.sort_by(&:index).each do |job|
info('%d: %s', job.index, job.mutation.identification)
end
end
# Return active subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def active_subject_results
active_subjects = active_jobs.map(&:mutation).flat_map(&:subject).to_set
env_result.subject_results.select do |subject_result|
active_subjects.include?(subject_result.subject)
end
end
end # Status
# Progress printer for configuration
class Config < self
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [self]
#
# @api private
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', object.matcher_config.inspect
info 'Integration: %s', object.integration.name
info 'Expect Coverage: %0.2f%%', object.expected_coverage.inspect
info 'Jobs: %d', object.jobs
info 'Includes: %s', object.includes.inspect
info 'Requires: %s', object.requires.inspect
self
end
end # Config
# Env progress printer
class EnvProgress < self
delegate(
:coverage,
:amount_subjects,
:amount_mutations,
:amount_mutations_alive,
:amount_mutations_killed,
:runtime,
:killtime,
:overhead,
:env
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
visit(Config, env.config)
info 'Available Subjects: %s', amount_subjects
info 'Subjects: %s', amount_subjects
info 'Mutations: %s', amount_mutations
info 'Kills: %s', amount_mutations_killed
info 'Alive: %s', amount_mutations_alive
info 'Runtime: %0.2fs', runtime
info 'Killtime: %0.2fs', killtime
info 'Overhead: %0.2f%%', overhead_percent
status 'Coverage: %0.2f%%', coverage_percent
status 'Expected: %0.2f%%', env.config.expected_coverage
self
end
private
# Return coverage percent
#
# @return [Float]
#
# @api private
#
def coverage_percent
coverage * 100
end
# Return overhead percent
#
# @return [Float]
#
# @api private
#
def overhead_percent
(overhead / killtime) * 100
end
end # EnvProgress
# Full env result reporter
class EnvResult < self
delegate(:failed_subject_results)
# Run printer
#
# @return [self]
#
# @api private
#
def run
visit_collection(SubjectResult, failed_subject_results)
visit(EnvProgress, object)
self
end
end # EnvResult
# Subject report printer
class SubjectResult < self
delegate :subject, :failed_mutations
# Run report printer
#
# @return [self]
#
# @api private
#
def run
status(subject.identification)
subject.tests.each do |test|
puts("- #{test.identification}")
end
visit_collection(MutationResult, object.alive_mutation_results)
self
end
end # Subject
# Printer for mutation progress results
class MutationProgressResult < self
SUCCESS = '.'.freeze
FAILURE = 'F'.freeze
# Run printer
#
# @return [self]
#
# @api private
#
def run
char(success? ? SUCCESS : FAILURE)
end
private
# Write colorized char
#
# @param [String] char
#
# @return [undefined]
#
# @api private
#
def char(char)
output.write(colorize(status_color, char))
end
end # MutationProgressResult
# Reporter for subject progress
class SubjectProgress < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
delegate(
:subject,
:coverage,
:runtime,
:amount_mutations_killed,
:amount_mutations,
:amount_mutation_results,
:killtime,
:overhead
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
puts("#{subject.identification} mutations: #{amount_mutations}")
print_tests
print_mutation_results
print_progress_bar_finish
print_stats
self
end
private
# Print stats
#
# @return [undefined]
#
# @api private
#
def print_stats
status(
FORMAT,
amount_mutations_killed,
amount_mutations,
coverage * 100,
killtime,
runtime,
overhead
)
end
# Print tests
#
# @return [undefined]
#
# @api private
#
def print_tests
subject.tests.each do |test|
puts "- #{test.identification}"
end
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts(NL) unless amount_mutation_results.zero?
end
# Print mutation results
#
# @return [undefined]
#
# @api private
#
def print_mutation_results
visit_collection(MutationProgressResult, object.mutation_results)
end
end # Subject
# Reporter for mutation results
class MutationResult < self
delegate :mutation, :failed_test_results
DIFF_ERROR_MESSAGE = 'BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!'.freeze
MAP = {
Mutant::Mutation::Evil => :evil_details,
Mutant::Mutation::Neutral => :neutral_details,
Mutant::Mutation::Noop => :noop_details
}.freeze
NEUTRAL_MESSAGE =
"--- Neutral failure ---\n" \
"Original code was inserted unmutated. And the test did NOT PASS.\n" \
"Your tests do not pass initially or you found a bug in mutant / unparser.\n" \
"Subject AST:\n" \
"%s\n" \
"Unparsed Source:\n" \
"%s\n" \
"Test Reports: %d\n"
NOOP_MESSAGE =
"---- Noop failure -----\n" \
"No code was inserted. And the test did NOT PASS.\n" \
"This is typically a problem of your specs not passing unmutated.\n" \
"Test Reports: %d\n"
FOOTER = '-----------------------'.freeze
# Run report printer
#
# @return [self]
#
# @api private
#
def run
puts(mutation.identification)
print_details
puts(FOOTER)
self
end
private
# Return details
#
# @return [undefined]
#
# @api private
#
def print_details
send(MAP.fetch(mutation.class))
end
# Return evil details
#
# @return [String]
#
# @api private
#
def evil_details
original, current = mutation.original_source, mutation.source
diff = Mutant::Diff.build(original, current)
diff = color? ? diff.colorized_diff : diff.diff
puts(diff || ['Original source:', original, 'Mutated Source:', current, DIFF_ERROR_MESSAGE])
end
# Noop details
#
# @return [String]
#
# @api private
#
def noop_details
info(NOOP_MESSAGE, failed_test_results.length)
visit_failed_test_results
end
# Neutral details
#
# @return [String]
#
# @api private
#
def neutral_details
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source, failed_test_results.length)
visit_failed_test_results
end
# Visit failed test results
#
# @return [undefined]
#
# @api private
#
def visit_failed_test_results
visit_collection(TestResult, failed_test_results)
end
end # MutationResult
# Test result reporter
class TestResult < self
delegate :test, :runtime, :mutation
# Run test result reporter
#
# @return [self]
#
# @api private
#
def run
status('- %s / runtime: %s', test.identification, runtime)
puts('Test Output:')
puts(object.output)
end
def success?
false
end
end # TestResult
end # Printer
end # CLI
end # Reporter
end # Mutant