Cleanup report printers

This commit is contained in:
Markus Schirp 2015-05-03 00:21:30 +00:00
parent add2269d10
commit ecefef6166
30 changed files with 1388 additions and 827 deletions

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1237
total_score: 1240

View file

@ -1,2 +1,2 @@
---
threshold: 30.9
threshold: 29.3

View file

@ -217,6 +217,16 @@ require 'mutant/reporter/null'
require 'mutant/reporter/trace'
require 'mutant/reporter/cli'
require 'mutant/reporter/cli/printer'
require 'mutant/reporter/cli/printer/config'
require 'mutant/reporter/cli/printer/env_result'
require 'mutant/reporter/cli/printer/env_progress'
require 'mutant/reporter/cli/printer/mutation_result'
require 'mutant/reporter/cli/printer/mutation_progress_result'
require 'mutant/reporter/cli/printer/subject_progress'
require 'mutant/reporter/cli/printer/subject_result'
require 'mutant/reporter/cli/printer/status'
require 'mutant/reporter/cli/printer/status_progressive'
require 'mutant/reporter/cli/printer/test_result'
require 'mutant/reporter/cli/tput'
require 'mutant/reporter/cli/format'
require 'mutant/zombifier'

View file

@ -53,11 +53,6 @@ module Mutant
#
# TODO: Move this to a callback registration
#
# Reporters other than CLI that might exist in future
# may only need the final report. So providing a noop callback
# registration makes more sense for these.
# As only CLI reporters exist currently I do not really care right now.
#
# @return [Float]
#
# @api private
@ -88,7 +83,7 @@ module Mutant
# @api private
#
def report(env)
Printer::EnvResult.run(output, env)
Printer::EnvResult.call(output, env)
self
end

View file

@ -69,7 +69,7 @@ module Mutant
#
def format(printer, object)
buffer = new_buffer
printer.run(Output.new(tty, buffer), object)
printer.call(Output.new(tty, buffer), object)
buffer.rewind
buffer.read
end

View file

@ -3,25 +3,14 @@ module Mutant
class CLI
# CLI runner status printer base class
class Printer
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object)
include AbstractType, Delegator, Adamantium::Flat, Concord.new(:output, :object), Procto.call(:run)
delegate(:success?)
private_class_method :new
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]
@ -67,7 +56,7 @@ module Mutant
# @api private
#
def visit(printer, object)
printer.run(output, object)
printer.call(output, object)
end
# Print an info line to output
@ -133,490 +122,6 @@ module Mutant
# @api private
#
alias_method :color?, :tty?
# Printer for runner status
class Status < self
delegate(:active_jobs, :payload)
# Print progress for collector
#
# @return [self]
#
# @api private
#
def run
visit(EnvProgress, payload)
job_status
info('Active subjects: %d', active_subject_results.length)
visit_collection(SubjectProgress, active_subject_results)
self
end
private
# Print worker status
#
# @return [undefined]
#
# @api private
#
def job_status
return if active_jobs.empty?
info('Active Jobs:')
active_jobs.sort_by(&:index).each do |job|
info('%d: %s', job.index, job.payload.identification)
end
end
# Return active subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def active_subject_results
active_mutation_jobs = active_jobs.select { |job| job.payload.kind_of?(Mutation) }
active_subjects = active_mutation_jobs.map(&:payload).flat_map(&:subject).to_set
payload.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
#
# rubocop:disable AbcSize
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', object.matcher.inspect
info 'Integration: %s', object.integration.name
info 'Expect Coverage: %0.2f%%', (object.expected_coverage * 100)
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
#
# rubocop:disable MethodLength
#
def run
visit(Config, env.config)
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 * 100)
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, :tests
# Run report printer
#
# @return [self]
#
# @api private
#
def run
status(subject.identification)
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 progressive output format on scheduler Status objects
class StatusProgressive < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
delegate(
:coverage,
:runtime,
:amount_mutations_killed,
:amount_mutations,
:amount_mutation_results,
:killtime,
:overhead
)
# Run printer
#
# @return [self]
#
# @api private
#
def run
status(
FORMAT,
amount_mutations_killed,
amount_mutations,
coverage * 100,
killtime,
runtime,
overhead
)
self
end
private
# Return object being printed
#
# @return [Result::Env]
#
# @api private
#
def object
super().payload
end
end
# Reporter for subject progress
class SubjectProgress < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
delegate(
:tests,
: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_mutation_results
print_progress_bar_finish
print_stats
print_tests
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
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, :test_result
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 Result:\n".freeze
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 Result:\n".freeze
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)
visit_test_result
end
# Neutral details
#
# @return [String]
#
# @api private
#
def neutral_details
info(NEUTRAL_MESSAGE, mutation.subject.node.inspect, mutation.source)
visit_test_result
end
# Visit failed test results
#
# @return [undefined]
#
# @api private
#
def visit_test_result
visit(TestResult, test_result)
end
end # MutationResult
# Test result reporter
class TestResult < self
delegate :tests, :runtime
# Run test result reporter
#
# @return [self]
#
# @api private
#
def run
status('- %d @ runtime: %s', tests.length, runtime)
tests.each do |test|
puts(" - #{test.identification}")
end
puts('Test Output:')
puts(object.output)
end
# Test if test result is successful
#
# Only used to determine color.
#
# @return [false]
#
# @api private
#
def success?
false
end
end # TestResult
end # Printer
end # CLI
end # Reporter

View file

@ -0,0 +1,32 @@
module Mutant
class Reporter
class CLI
class Printer
# Printer for mutation config
class Config < self
# Report configuration
#
# @param [Mutant::Config] config
#
# @return [undefined]
#
# @api private
#
# rubocop:disable AbcSize
#
def run
info 'Mutant configuration:'
info 'Matcher: %s', object.matcher.inspect
info 'Integration: %s', object.integration.name
info 'Expect Coverage: %0.2f%%', (object.expected_coverage * 100)
info 'Jobs: %d', object.jobs
info 'Includes: %s', object.includes
info 'Requires: %s', object.requires
end
end # Config
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,66 @@
module Mutant
class Reporter
class CLI
class Printer
# 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 [undefined]
#
# @api private
#
# rubocop:disable MethodLength
# rubocop:disable AbcSize
#
def run
visit(Config, env.config)
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 * 100)
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
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,23 @@
module Mutant
class Reporter
class CLI
class Printer
# Full env result reporter
class EnvResult < self
delegate(:failed_subject_results)
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
visit_collection(SubjectResult, failed_subject_results)
visit(EnvProgress, object)
end
end # EnvResult
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,37 @@
module Mutant
class Reporter
class CLI
class Printer
# Printer for mutation progress results
class MutationProgressResult < self
SUCCESS = '.'.freeze
FAILURE = 'F'.freeze
# Run printer
#
# @return [undefined]
#
# @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
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,151 @@
module Mutant
class Reporter
class CLI
class Printer
# Reporter for mutation results
class MutationResult < self
delegate :mutation, :test_result
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 Result:\n".freeze
NO_DIFF_MESSAGE =
"--- Internal failure ---\n" \
"BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!\n" \
"Original unparsed source:\n" \
"%s\n" \
"Original AST:\n" \
"%s\n" \
"Mutated unparsed source:\n" \
"%s\n" \
"Mutated AST:\n" \
"%s\n".freeze
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 Result:\n".freeze
FOOTER = '-----------------------'.freeze
# Run report printer
#
# @return [undefined]
#
# @api private
#
def run
puts(mutation.identification)
print_details
puts(FOOTER)
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
diff = Diff.build(mutation.original_source, mutation.source)
diff = color? ? diff.colorized_diff : diff.diff
if diff
output.write(diff)
else
print_no_diff_message
end
end
# Print no diff message
#
# @return [undefined]
#
# @api private
#
def print_no_diff_message
info(
NO_DIFF_MESSAGE,
mutation.original_source,
original_node.inspect,
mutation.source,
mutation.node.inspect
)
end
# Noop details
#
# @return [String]
#
# @api private
#
def noop_details
info(NOOP_MESSAGE)
visit_test_result
end
# Neutral details
#
# @return [String]
#
# @api private
#
def neutral_details
info(NEUTRAL_MESSAGE, original_node.inspect, mutation.source)
visit_test_result
end
# Visit failed test results
#
# @return [undefined]
#
# @api private
#
def visit_test_result
visit(TestResult, test_result)
end
# Return original node
#
# @return [Parser::AST::Node]
#
# @api private
#
def original_node
mutation.subject.node
end
end # MutationResult
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,60 @@
module Mutant
class Reporter
class CLI
class Printer
# Printer for runner status
class Status < self
delegate(:active_jobs, :payload)
ACTIVE_JOB_HEADER = 'Active Jobs:'.freeze
ACTIVE_JOB_FORMAT = '%d: %s'.freeze
# Print progress for collector
#
# @return [undefined]
#
# @api private
#
def run
visit(EnvProgress, payload)
job_status
info('Active subjects: %d', active_subject_results.length)
visit_collection(SubjectProgress, active_subject_results)
end
private
# Print worker status
#
# @return [undefined]
#
# @api private
#
def job_status
return if active_jobs.empty?
info(ACTIVE_JOB_HEADER)
active_jobs.sort_by(&:index).each do |job|
info(ACTIVE_JOB_FORMAT, job.index, job.payload.identification)
end
end
# Return active subject results
#
# @return [Array<Result::Subject>]
#
# @api private
#
def active_subject_results
active_subjects = active_jobs.map(&:payload).flat_map(&:subject)
payload.subject_results.select do |subject_result|
active_subjects.include?(subject_result.subject)
end
end
end # Status
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,52 @@
module Mutant
class Reporter
class CLI
class Printer
# Reporter for progressive output format on scheduler Status objects
class StatusProgressive < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
delegate(
:coverage,
:runtime,
:amount_mutations_killed,
:amount_mutations,
:amount_mutation_results,
:killtime,
:overhead
)
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
status(
FORMAT,
amount_mutations_killed,
amount_mutations,
coverage * 100,
killtime,
runtime,
overhead
)
end
private
# Return object being printed
#
# @return [Result::Env]
#
# @api private
#
def object
super.payload
end
end # StatusProgressive
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,90 @@
module Mutant
class Reporter
class CLI
class Printer
# Reporter for subject progress
class SubjectProgress < self
FORMAT = '(%02d/%02d) %3d%% - killtime: %0.02fs runtime: %0.02fs overhead: %0.02fs'.freeze
delegate(
:tests,
:subject,
:coverage,
:runtime,
:amount_mutations_killed,
:amount_mutations,
:amount_mutation_results,
:killtime,
:overhead
)
# Run printer
#
# @return [undefined]
#
# @api private
#
def run
puts("#{subject.identification} mutations: #{amount_mutations}")
print_mutation_results
print_progress_bar_finish
print_stats
print_tests
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
tests.each do |test|
puts "- #{test.identification}"
end
end
# Print progress bar finish
#
# @return [undefined]
#
# @api private
#
def print_progress_bar_finish
puts(nil) 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 # SubjectProgress
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,28 @@
module Mutant
class Reporter
class CLI
class Printer
# Subject result printer
class SubjectResult < self
delegate :subject, :alive_mutation_results, :tests
# Run report printer
#
# @return [undefined]
#
# @api private
#
def run
status(subject.identification)
tests.each do |test|
puts("- #{test.identification}")
end
visit_collection(MutationResult, alive_mutation_results)
end
end # SubjectResult
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -0,0 +1,33 @@
module Mutant
class Reporter
class CLI
class Printer
# Test result reporter
class TestResult < self
delegate :tests, :runtime
STATUS_FORMAT = '- %d @ runtime: %s'.freeze
OUTPUT_HEADER = 'Test Output:'.freeze
TEST_FORMAT = ' - %s'.freeze
# Run test result reporter
#
# @return [undefined]
#
# @api private
#
def run
info(STATUS_FORMAT, tests.length, runtime)
tests.each do |test|
info(TEST_FORMAT, test.identification)
end
puts(OUTPUT_HEADER)
puts(object.output)
end
end # TestResult
end # Printer
end # CLI
end # Reporter
end # Mutant

View file

@ -14,6 +14,14 @@ module SharedContext
end
end
def it_reports(expected_content)
it 'writes expected report to output' do
described_class.call(output, reportable)
output.rewind
expect(output.read).to eql(strip_indent(expected_content))
end
end
# rubocop:disable MethodLength
# rubocop:disable AbcSize
def setup_shared_context
@ -24,6 +32,7 @@ module SharedContext
let(:job_b_result) { Mutant::Runner::JobResult.new(job: job_b, result: mutation_b_result) }
let(:test_a) { double('test a', identification: 'test-a') }
let(:test_b) { double('test b', identification: 'test-b') }
let(:output) { StringIO.new }
let(:matchable_scopes) { double('matchable scopes', length: 10) }
let(:message_sequence) { FakeActor::MessageSequence.new }
let(:mutations) { [mutation_a, mutation_b] }
@ -31,10 +40,7 @@ module SharedContext
let(:mutation_b_node) { s(:nil) }
let(:mutation_b) { Mutant::Mutation::Evil.new(subject_a, mutation_b_node) }
let(:mutation_a) { Mutant::Mutation::Evil.new(subject_a, mutation_a_node) }
before do
allow(subject_a).to receive(:mutations).and_return([mutation_a, mutation_b])
end
let(:subject_a_node) { s(:true) }
let(:status) do
Mutant::Parallel::Status.new(
@ -54,13 +60,17 @@ module SharedContext
let(:subject_a) do
double(
'subject a',
node: s(:true),
source: 'true',
node: subject_a_node,
source: Unparser.unparse(subject_a_node),
tests: [test_a],
identification: 'subject-a'
)
end
before do
allow(subject_a).to receive(:mutations).and_return([mutation_a, mutation_b])
end
let(:env_result) do
Mutant::Result::Env.new(
env: env,

View file

@ -0,0 +1,33 @@
RSpec.describe Mutant::Reporter::CLI::Printer::Config do
setup_shared_context
let(:reportable) { config }
describe '.call' do
context 'on default config' do
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
REPORT
end
context 'with non default coverage expectation' do
update(:config) { { expected_coverage: 0.1r } }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
REPORT
end
end
end

View file

@ -0,0 +1,76 @@
RSpec.describe Mutant::Reporter::CLI::Printer::EnvProgress do
setup_shared_context
update(:config) { { expected_coverage: 0.1r } }
let(:reportable) { env_result }
describe '.call' do
context 'without progress' do
update(:subject_a_result) { { mutation_results: [] } }
it_reports <<-'STR'
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 0
Runtime: 4.00s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 10.00%
STR
end
context 'on full coverage' do
it_reports <<-'STR'
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 10.00%
STR
end
context 'on partial coverage' do
update(:mutation_a_test_result) { { passed: true } }
it_reports <<-'STR'
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 10.00%
STR
end
end
end

View file

@ -0,0 +1,35 @@
RSpec.describe Mutant::Reporter::CLI::Printer::EnvResult do
setup_shared_context
update(:mutation_a_test_result) { { passed: true } }
let(:reportable) { env_result }
describe '.call' do
it_reports <<-'STR'
subject-a
- test-a
evil:subject-a:d27d2
@@ -1,2 +1,2 @@
-true
+false
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 100.00%
STR
end
end

View file

@ -0,0 +1,23 @@
RSpec.describe Mutant::Reporter::CLI::Printer::MutationProgressResult do
setup_shared_context
let(:reportable) { mutation_a_result }
before do
allow(output).to receive(:tty?).and_return(true)
end
describe '.run' do
context 'on killed mutant' do
update(:mutation_a_test_result) { { passed: true } }
it_reports Mutant::Color::RED.format('F')
end
context 'on alive mutant' do
update(:mutation_a_test_result) { { passed: false } }
it_reports Mutant::Color::GREEN.format('.')
end
end
end

View file

@ -0,0 +1,110 @@
RSpec.describe Mutant::Reporter::CLI::Printer::MutationResult do
setup_shared_context
let(:reportable) { mutation_a_result }
describe '.call' do
context 'failed kill' do
update(:mutation_a_test_result) { { passed: true } }
context 'on evil mutation' do
context 'with a diff' do
context 'on a tty' do
before do
allow(output).to receive(:tty?).and_return(true)
end
it_reports(
[
[Mutant::Color::NONE, "evil:subject-a:d27d2\n"],
[Mutant::Color::NONE, "@@ -1,2 +1,2 @@\n"],
[Mutant::Color::RED, "-true\n"],
[Mutant::Color::GREEN, "+false\n"],
[Mutant::Color::NONE, "-----------------------\n"]
].map { |color, text| color.format(text) }.join
)
end
context 'on non tty' do
it_reports(<<-'STR')
evil:subject-a:d27d2
@@ -1,2 +1,2 @@
-true
+false
-----------------------
STR
end
end
context 'without a diff' do
# This is intentionally invalid AST mutant might produce
let(:subject_a_node) { s(:lvar, :super) }
# Unparses exactly the same way as above node
let(:mutation_a_node) { s(:zsuper) }
it_reports(<<-REPORT)
evil:subject-a:a5bc7
--- Internal failure ---
BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!
Original unparsed source:
super
Original AST:
(lvar :super)
Mutated unparsed source:
super
Mutated AST:
(zsuper)
-----------------------
REPORT
end
end
context 'on neutral mutation' do
update(:mutation_a_test_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Neutral.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
neutral:subject-a:d5318
--- Neutral failure ---
Original code was inserted unmutated. And the test did NOT PASS.
Your tests do not pass initially or you found a bug in mutant / unparser.
Subject AST:
(true)
Unparsed Source:
true
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation a test result output
-----------------------
REPORT
end
context 'on noop mutation' do
update(:mutation_a_test_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Noop.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
noop:subject-a:d5318
---- Noop failure -----
No code was inserted. And the test did NOT PASS.
This is typically a problem of your specs not passing unmutated.
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation a test result output
-----------------------
REPORT
end
end
end
end

View file

@ -0,0 +1,51 @@
RSpec.describe Mutant::Reporter::CLI::Printer::StatusProgressive do
setup_shared_context
let(:reportable) { status }
describe '.call' do
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
it_reports <<-REPORT
(00/02) 0% - killtime: 0.00s runtime: 4.00s overhead: 4.00s
REPORT
context 'on non default coverage expectation' do
update(:config) { { expected_coverage: 0.1r } }
it_reports <<-REPORT
(00/02) 0% - killtime: 0.00s runtime: 4.00s overhead: 4.00s
REPORT
end
end
context 'with scheduler active on one subject' do
context 'without progress' do
update(:status) { { active_jobs: [].to_set } }
it_reports(<<-REPORT)
(02/02) 100% - killtime: 2.00s runtime: 4.00s overhead: 2.00s
REPORT
end
context 'with progress' do
update(:status) { { active_jobs: [job_b, job_a].to_set } }
context 'on failure' do
update(:mutation_a_test_result) { { passed: true } }
it_reports(<<-REPORT)
(01/02) 50% - killtime: 2.00s runtime: 4.00s overhead: 2.00s
REPORT
end
context 'on success' do
it_reports(<<-REPORT)
(02/02) 100% - killtime: 2.00s runtime: 4.00s overhead: 2.00s
REPORT
end
end
end
end
end

View file

@ -0,0 +1,145 @@
RSpec.describe Mutant::Reporter::CLI::Printer::Status do
setup_shared_context
let(:reportable) { status }
describe '.call' do
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
it_reports <<-REPORT
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 0
Runtime: 4.00s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 0
REPORT
context 'on non default coverage expectation' do
update(:config) { { expected_coverage: 0.1r } }
it_reports <<-REPORT
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 0
Runtime: 4.00s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 10.00%
Active subjects: 0
REPORT
end
end
context 'with scheduler active on one subject' do
context 'without progress' do
update(:status) { { active_jobs: [].to_set } }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
context 'with progress' do
update(:status) { { active_jobs: [job_b, job_a].to_set } }
context 'on failure' do
update(:mutation_a_test_result) { { passed: true } }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 100.00%
Active Jobs:
0: evil:subject-a:d27d2
1: evil:subject-a:d5a9d
Active subjects: 1
subject-a mutations: 2
F.
(01/02) 50% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
REPORT
end
context 'on success' do
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
Active Jobs:
0: evil:subject-a:d27d2
1: evil:subject-a:d5a9d
Active subjects: 1
subject-a mutations: 2
..
(02/02) 100% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
REPORT
end
end
end
end
end

View file

@ -0,0 +1,37 @@
RSpec.describe Mutant::Reporter::CLI::Printer::SubjectProgress do
setup_shared_context
let(:reportable) { subject_a_result }
describe '.call' do
context 'on full coverage' do
it_reports <<-'STR'
subject-a mutations: 2
..
(02/02) 100% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
STR
end
context 'on partial coverage' do
update(:mutation_a_test_result) { { passed: true } }
it_reports <<-'STR'
subject-a mutations: 2
F.
(01/02) 50% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
STR
end
context 'without results' do
update(:subject_a_result) { { mutation_results: [] } }
it_reports <<-'STR'
subject-a mutations: 2
(00/02) 0% - killtime: 0.00s runtime: 0.00s overhead: 0.00s
- test-a
STR
end
end
end

View file

@ -0,0 +1,37 @@
RSpec.describe Mutant::Reporter::CLI::Printer::SubjectResult do
setup_shared_context
let(:reportable) { subject_a_result }
describe '.call' do
context 'on full coverage' do
it_reports <<-'STR'
subject-a
- test-a
STR
end
context 'on partial coverage' do
update(:mutation_a_test_result) { { passed: true } }
it_reports <<-'STR'
subject-a
- test-a
evil:subject-a:d27d2
@@ -1,2 +1,2 @@
-true
+false
-----------------------
STR
end
context 'without results' do
update(:subject_a_result) { { mutation_results: [] } }
it_reports <<-'STR'
subject-a
- test-a
STR
end
end
end

View file

@ -0,0 +1,14 @@
RSpec.describe Mutant::Reporter::CLI::Printer::TestResult do
setup_shared_context
let(:reportable) { mutation_a_test_result }
describe '.call' do
it_reports <<-'STR'
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation a test result output
STR
end
end

View file

@ -0,0 +1,140 @@
RSpec.describe Mutant::Reporter::CLI::Printer do
let(:output) { StringIO.new }
subject { class_under_test.call(output, reportable) }
def self.it_reports(expectation)
it 'writes expected report' do
allow(output).to receive(:tty?).and_return(tty?)
subject
output.rewind
expect(output.read).to eql(strip_indent(expectation))
end
end
let(:reportable) { double('Reportable', success?: success?) }
let(:tty?) { true }
let(:success?) { true }
context '.call' do
let(:class_under_test) do
Class.new(described_class) do
def run
puts object
end
end
end
let(:reportable) { 'foo' }
it_reports "foo\n"
end
context '#status' do
let(:class_under_test) do
Class.new(described_class) do
def run
status('foo %s', 'bar')
end
end
end
context 'on tty' do
context 'on success' do
it_reports Mutant::Color::GREEN.format('foo bar') << "\n"
end
context 'on failure' do
let(:success?) { false }
it_reports Mutant::Color::RED.format('foo bar') << "\n"
end
end
context 'on no tty' do
let(:tty?) { false }
context 'on success' do
it_reports "foo bar\n"
end
context 'on failure' do
let(:success?) { false }
it_reports "foo bar\n"
end
end
end
context '#visit_collection' do
let(:class_under_test) do
reporter = nested_reporter
Class.new(described_class) do
define_method(:run) do
visit_collection(reporter, %w[foo bar])
end
end
end
let(:nested_reporter) do
Class.new(described_class) do
def run
puts object
end
end
end
it_reports "foo\nbar\n"
end
context '#visit' do
let(:class_under_test) do
reporter = nested_reporter
Class.new(described_class) do
define_method(:run) do
visit(reporter, 'foo')
end
end
end
let(:nested_reporter) do
Class.new(described_class) do
def run
puts object
end
end
end
it_reports "foo\n"
end
context '#info' do
let(:class_under_test) do
Class.new(described_class) do
def run
info('%s - %s', 'foo', 'bar')
end
end
end
it_reports "foo - bar\n"
end
context '#colorize' do
let(:class_under_test) do
Class.new(described_class) do
def run
puts(colorize(Mutant::Color::RED, 'foo'))
end
end
end
context 'when output is a tty?' do
it_reports Mutant::Color::RED.format('foo') << "\n"
end
context 'when output is NOT a tty?' do
let(:tty?) { false }
it_reports "foo\n"
end
end
end

View file

@ -19,8 +19,10 @@ RSpec.describe Mutant::Reporter::CLI do
)
end
let(:tty?) { false }
let(:progressive_format) do
described_class::Format::Progressive.new(tty: false)
described_class::Format::Progressive.new(tty: tty?)
end
let(:format) { framed_format }
@ -128,21 +130,85 @@ RSpec.describe Mutant::Reporter::CLI do
REPORT
end
context 'with non default coverage expectation' do
let(:format) { progressive_format }
update(:config) { { expected_coverage: 0.1r } }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 10.00%
Jobs: 1
Includes: []
Requires: []
REPORT
end
context 'on framed format' do
it_reports '[tput-prepare]'
end
end
describe '#report' do
subject { object.report(env_result) }
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
REPORT
end
describe '#progress' do
subject { object.progress(status) }
context 'on framed format' do
let(:format) { framed_format }
it_reports(<<-REPORT)
[tput-restore]Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
context 'on progressive format' do
let(:format) { progressive_format }
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
it_reports "(00/02) 0% - killtime: 0.00s runtime: 4.00s overhead: 4.00s\n"
let(:tty?) { true }
it_reports Mutant::Color::RED.format('(00/02) 0% - killtime: 0.00s runtime: 4.00s overhead: 4.00s') << "\n"
end
context 'with last mutation present' do
@ -159,315 +225,5 @@ RSpec.describe Mutant::Reporter::CLI do
end
end
context 'on framed format' do
context 'with empty scheduler' do
update(:env_result) { { subject_results: [] } }
it_reports <<-REPORT
[tput-restore]Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 0
Runtime: 4.00s
Killtime: 0.00s
Overhead: Inf%
Coverage: 0.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
context 'with scheduler active on one subject' do
context 'without progress' do
update(:status) { { active_jobs: [].to_set } }
it_reports(<<-REPORT)
[tput-restore]Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
Active subjects: 0
REPORT
end
context 'with progress' do
update(:status) { { active_jobs: [job_a].to_set } }
context 'on failure' do
update(:mutation_a_test_result) { { passed: true } }
it_reports(<<-REPORT)
[tput-restore]Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 100.00%
Active Jobs:
0: evil:subject-a:d27d2
Active subjects: 1
subject-a mutations: 2
F.
(01/02) 50% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
REPORT
end
context 'on success' do
it_reports(<<-REPORT)
[tput-restore]Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
Active Jobs:
0: evil:subject-a:d27d2
Active subjects: 1
subject-a mutations: 2
..
(02/02) 100% - killtime: 2.00s runtime: 2.00s overhead: 0.00s
- test-a
REPORT
end
end
end
end
describe '#report' do
subject { object.report(status.payload) }
context 'with full coverage' do
it_reports(<<-REPORT)
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 2
Alive: 0
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 100.00%
Expected: 100.00%
REPORT
end
context 'and partial coverage' do
update(:mutation_a_test_result) { { passed: true } }
context 'on evil mutation' do
context 'with a diff' do
it_reports(<<-REPORT)
subject-a
- test-a
evil:subject-a:d27d2
@@ -1,2 +1,2 @@
-true
+false
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 100.00%
REPORT
end
context 'without a diff' do
let(:mutation_a_node) { s(:true) }
it_reports(<<-REPORT)
subject-a
- test-a
evil:subject-a:d5318
Original source:
true
Mutated Source:
true
BUG: Mutation NOT resulted in exactly one diff hunk. Please report a reproduction!
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 1
Alive: 1
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 50.00%
Expected: 100.00%
REPORT
end
end
context 'on neutral mutation' do
update(:mutation_a_test_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Neutral.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
subject-a
- test-a
neutral:subject-a:d5318
--- Neutral failure ---
Original code was inserted unmutated. And the test did NOT PASS.
Your tests do not pass initially or you found a bug in mutant / unparser.
Subject AST:
(true)
Unparsed Source:
true
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation a test result output
-----------------------
neutral:subject-a:d5318
--- Neutral failure ---
Original code was inserted unmutated. And the test did NOT PASS.
Your tests do not pass initially or you found a bug in mutant / unparser.
Subject AST:
(true)
Unparsed Source:
true
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation b test result output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 2
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
context 'on noop mutation' do
update(:mutation_a_test_result) { { passed: false } }
let(:mutation_a) do
Mutant::Mutation::Noop.new(subject_a, s(:true))
end
it_reports(<<-REPORT)
subject-a
- test-a
noop:subject-a:d5318
---- Noop failure -----
No code was inserted. And the test did NOT PASS.
This is typically a problem of your specs not passing unmutated.
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation a test result output
-----------------------
noop:subject-a:d5318
---- Noop failure -----
No code was inserted. And the test did NOT PASS.
This is typically a problem of your specs not passing unmutated.
Test Result:
- 1 @ runtime: 1.0
- test-a
Test Output:
mutation b test result output
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions=[] subject_ignores=[] subject_selects=[]>
Integration: null
Expect Coverage: 100.00%
Jobs: 1
Includes: []
Requires: []
Subjects: 1
Mutations: 2
Kills: 0
Alive: 2
Runtime: 4.00s
Killtime: 2.00s
Overhead: 100.00%
Coverage: 0.00%
Expected: 100.00%
REPORT
end
end
end
end
end

View file

@ -6,4 +6,16 @@ RSpec.describe Mutant::Reporter::Trace do
it { should eql(0.0) }
end
let(:reportable) { double('Reportable') }
%i[report start progress].each do |name|
describe "##{name}" do
subject { object.public_send(name, reportable) }
it 'logs the reportable' do
expect { subject }.to change { object.public_send("#{name}_calls") }.from([]).to([reportable])
end
end
end
end