Skip to content

Commit

Permalink
Add util subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Aug 25, 2021
1 parent d6a4053 commit 441cbe4
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 32 deletions.
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# v0.10.33 2021-08-25

* [#1249](https://github.com/mbj/mutant/pull/1249/files)
Add `mutant util mutation` subcommand to allow inspect mutations of
a code snippet outside a booted environment.
This eases debugging, learning and mutant developers life.

# v0.10.32 2021-05-16

* [#1235](https://github.com/mbj/mutant/pull/1235)
Expand Down
2 changes: 2 additions & 0 deletions lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ module Mutant
require 'mutant/cli/command/environment/show'
require 'mutant/cli/command/environment/subject'
require 'mutant/cli/command/environment/test'
require 'mutant/cli/command/util'
require 'mutant/cli/command/root'
require 'mutant/runner'
require 'mutant/runner/sink'
Expand All @@ -212,6 +213,7 @@ module Mutant
require 'mutant/reporter/cli/printer/env_progress'
require 'mutant/reporter/cli/printer/env_result'
require 'mutant/reporter/cli/printer/isolation_result'
require 'mutant/reporter/cli/printer/mutation'
require 'mutant/reporter/cli/printer/mutation_result'
require 'mutant/reporter/cli/printer/status_progressive'
require 'mutant/reporter/cli/printer/subject_result'
Expand Down
2 changes: 2 additions & 0 deletions lib/mutant/cli/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def zombie?
false
end

abstract_method :action

private

def subcommands
Expand Down
2 changes: 1 addition & 1 deletion lib/mutant/cli/command/root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Environment < self
class Root < self
NAME = 'mutant'
SHORT_DESCRIPTION = 'mutation testing engine main command'
SUBCOMMANDS = [Environment::Run, Environment, Subscription].freeze
SUBCOMMANDS = [Environment::Run, Environment, Subscription, Util].freeze
end # Root
end # Command
end # CLI
Expand Down
105 changes: 105 additions & 0 deletions lib/mutant/cli/command/util.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module Mutant
module CLI
class Command
class Util < self
NAME = 'util'
SHORT_DESCRIPTION = 'Utility subcommands'

class Mutation < self
NAME = 'mutation'
SHORT_DESCRIPTION = 'Print mutations of a code snippet'
SUBCOMMANDS = [].freeze
OPTIONS = %i[add_target_options].freeze

def action
@targets.each(&method(:print_mutations))
Either::Right.new(nil)
end

private

class Target
include Adamantium

def node
Unparser.parse(source)
end
memoize :node

class File < self
include Concord.new(:pathname, :source)

public :source

def identification
"file:#{pathname}"
end
end # File

class Source < self
include Concord::Public.new(:source)

def identification
'<cli-source>'
end
end # source
end # Target

def initialize(_arguments)
super

@targets = []
end

def add_target_options(parser)
parser.on('-e', '--evaluate SOURCE') do |source|
@targets << Target::Source.new(source)
end
end

def print_mutations(target)
world.stdout.puts(target.identification)
Mutator.mutate(target.node).each do |mutation|
Reporter::CLI::Printer::Mutation.call(
world.stdout,
Mutant::Mutation::Evil.new(target, mutation)
)
end
end

def parse_remaining_arguments(arguments)
@targets.concat(
arguments.map do |argument|
parse_pathname(argument)
.bind(&method(:read_file))
.from_right { |error| return Either::Left.new(error) }
end
)

Either::Right.new(self)
end

def read_file(pathname)
source =
begin
pathname.read
rescue Exception => exception
return Either::Left.new("Cannot read file: #{exception}")
end

Either::Right.new(Target::File.new(pathname, source))
end

def parse_pathname(input)
Either.wrap_error(ArgumentError) { Pathname.new(input) }
.lmap(&:message)
end
end # Mutation

SUBCOMMANDS = [Mutation]
end # Util
end # Command
end # CLI
end # Mutant
10 changes: 9 additions & 1 deletion lib/mutant/mutation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ def insert(kernel)
)
end

# Rendered mutation diff
#
# @return [String, nil]
# the diff, if present
def diff
Unparser::Diff.build(original_source, source)
end
memoize :diff

private

def sha1
Expand All @@ -80,7 +89,6 @@ def sha1

# Evil mutation that should case mutations to fail tests
class Evil < self

SYMBOL = 'evil'
TEST_PASS_SUCCESS = false

Expand Down
58 changes: 58 additions & 0 deletions lib/mutant/reporter/cli/printer/mutation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

module Mutant
class Reporter
class CLI
class Printer
# Reporter for mutations
class Mutation < self
NO_DIFF_MESSAGE = <<~'MESSAGE'
--- Internal failure ---
BUG: A generted mutation did not result in exactly one diff hunk!
This is an invariant violation by the mutation generation engine.
Please report a reproduction to https://github.com/mbj/mutant
Original unparsed source:
%s
Original AST:
%s
Mutated unparsed source:
%s
Mutated AST:
%s
MESSAGE

SEPARATOR = '-----------------------'

# Run report printer
#
# @return [undefined]
def run
diff = object.diff
diff = color? ? diff.colorized_diff : diff.diff

if diff
output.write(diff)
else
print_no_diff_message
end
end

def print_no_diff_message
info(
NO_DIFF_MESSAGE,
object.original_source,
original_node.inspect,
object.source,
object.node.inspect
)
end

def original_node
object.subject.node
end

end # MutationResult
end # Printer
end # CLI
end # Reporter
end # Mutant
24 changes: 2 additions & 22 deletions lib/mutant/reporter/cli/printer/mutation_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,35 +66,15 @@ def print_details
end

def evil_details
diff = Unparser::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

def print_no_diff_message
info(
NO_DIFF_MESSAGE,
mutation.original_source,
original_node.inspect,
mutation.source,
mutation.node.inspect
)
visit(Mutation, mutation)
end

def noop_details
info(NOOP_MESSAGE)
end

def neutral_details
info(NEUTRAL_MESSAGE, original_node.inspect, mutation.source)
end

def original_node
mutation.subject.node
info(NEUTRAL_MESSAGE, mutation.node.inspect, mutation.source)
end

end # MutationResult
Expand Down
Loading

0 comments on commit 441cbe4

Please sign in to comment.