Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to unparser 0.7.x interface #1462

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# v0.13.0 unreleased

Significant unparser upgrade. Mutant now:

Avoids emitting mutations that do not round trip against unparsers API.

This change generates less mutations, and currently slightly increases boot time.
But the number of mutations that are hitting the test suite is much lower so it
should balance.

Also its a good step towards parallel mutation generation reducing boot times.

Also mutations that are currently not creating round trippable ASTS are removed:

* Negation of `if` conditions, as negating these needs operator precendence sensitive AST
mutations mutant does not have the infrastructure for right now. The mutation to `unless` is
still present so there is no real reduction of semantic coverage.

* All mutations that modify the local variable scope are removed. These generated ASTs that would
not round trip and are thus likely to be covered, execution wise these would move lvar reads to
implicit self receivers in the past. But this was not intended by these mutations, at least not
without explicitly changing the reads to send nodes explicitly.

# v0.12.5 unreleased

* [#1458](https://github.com/mbj/mutant/pull/1458)
Expand Down
45 changes: 23 additions & 22 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
PATH
remote: ../unparser
specs:
unparser (0.7.0)
diff-lcs (~> 1.3)
parser (>= 3.3.0)

PATH
remote: .
specs:
Expand All @@ -6,60 +13,53 @@ PATH
parser (~> 3.3.0)
regexp_parser (~> 2.9.0)
sorbet-runtime (~> 0.5.0)
unparser (~> 0.6.14)
unparser (~> 0.7.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
diff-lcs (1.5.1)
json (2.7.2)
json (2.7.5)
language_server-protocol (3.17.0.3)
parallel (1.25.1)
parser (3.3.2.0)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
racc (1.8.0)
racc (1.8.1)
rainbow (3.1.1)
regexp_parser (2.9.2)
rexml (3.2.9)
strscan
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-its (1.3.0)
rspec-its (1.3.1)
rspec-core (>= 3.0.0)
rspec-expectations (>= 3.0.0)
rspec-mocks (3.13.1)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.64.1)
rubocop (1.67.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
rubocop-ast (1.33.0)
parser (>= 3.3.1.0)
ruby-progressbar (1.13.0)
sorbet-runtime (0.5.11422)
strscan (3.1.0)
unicode-display_width (2.5.0)
unparser (0.6.14)
diff-lcs (~> 1.3)
parser (>= 3.3.0)
sorbet-runtime (0.5.11625)
unicode-display_width (2.6.0)

PLATFORMS
ruby
Expand All @@ -70,6 +70,7 @@ DEPENDENCIES
rspec-core (~> 3.10)
rspec-its (~> 1.3.0)
rubocop (~> 1.7)
unparser!

BUNDLED WITH
2.5.6
1 change: 1 addition & 0 deletions Gemfile.shared
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gem 'unparser', path: '../unparser'
23 changes: 18 additions & 5 deletions lib/mutant/cli/command/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ def action
end
end

@targets.each(&method(:print_mutations))
Either::Right.new(nil)
if @targets.map(&method(:print_mutations)).all?
Either::Right.new(nil)
else
Either::Left.new('Invalid mutation detected!')
end
end

private
Expand Down Expand Up @@ -70,18 +73,28 @@ def add_target_options(parser)
end
end

# rubocop:disable Metrics/MethodLength
def print_mutations(target)
world.stdout.puts(target.identification)

success = true

Mutator::Node.mutate(
config: Mutant::Mutation::Config::DEFAULT.with(ignore_patterns: @ignore_patterns),
node: target.node
).each do |mutation|
Reporter::CLI::Printer::Mutation.call(
object: Mutant::Mutation::Evil.new(subject: target, node: mutation),
output: world.stdout
Mutant::Mutation::Evil.from_node(subject: target, node: mutation).either(
->(violation) { world.stdout.puts(violation.report); success = false },
lambda { |object|
Reporter::CLI::Printer::Mutation.call(
object:,
output: world.stdout
)
}
)
end

success
end

def parse_remaining_arguments(arguments)
Expand Down
10 changes: 5 additions & 5 deletions lib/mutant/meta/example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Expected
#
# @return [Verification]
def verification
Verification.new(example: self, mutations: generated)
Verification.from_mutations(example: self, mutations: generated)
end
memoize :verification

Expand All @@ -45,13 +45,13 @@ def context
)
end

# Original source as generated by unparser
# Original source
#
# @return [String]
def original_source_generated
def source
Unparser.unparse(node)
end
memoize :original_source_generated
memoize :source

# Generated mutations on example source
#
Expand All @@ -61,7 +61,7 @@ def generated
config: Mutation::Config::DEFAULT.with(operators:),
node:
).map do |node|
Mutation::Evil.new(subject: self, node:)
Mutation::Evil.from_node(subject: self, node:)
end
end
memoize :generated
Expand Down
49 changes: 32 additions & 17 deletions lib/mutant/meta/example/verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@

module Mutant
module Meta
# rubocop:disable Metrics/ClassLength
class Example
# Example verification
class Verification
include Adamantium, Anima.new(:example, :mutations)
include Adamantium, Anima.new(:example, :invalid, :valid)

def self.from_mutations(example:, mutations:)
valid, invalid = [], []

mutations.each do |mutation|
mutation.either(invalid.public_method(:<<), valid.public_method(:<<))
end

new(example:, invalid:, valid:)
end

# Test if mutation was verified successfully
#
# @return [Boolean]
def success?
[
original_verification,
invalid,
invalid_report,
missing,
no_diffs,
original_verification_report,
unexpected
].all?(&:empty?)
end
Expand All @@ -29,12 +40,12 @@ def error_report

def reports
reports = [example.location]
reports.concat(original)
reports.concat(original_verification)
reports.concat(original_report)
reports.concat(original_verification_report)
reports.concat(make_report('Missing mutations:', missing))
reports.concat(make_report('Unexpected mutations:', unexpected))
reports.concat(make_report('No-Diff mutations:', no_diffs))
reports.concat(invalid)
reports.concat(invalid_report)
end

def make_report(label, mutations)
Expand All @@ -52,15 +63,15 @@ def report_mutation(mutation)
]
end

def original
def original_report
[
"Original: (operators: #{example.operators.class.operators_name})",
example.node,
example.original_source
]
end

def original_verification
def original_verification_report
validation = Unparser::Validation.from_string(example.original_source)
if validation.success?
[]
Expand All @@ -77,30 +88,34 @@ def prefix(prefix, string)
end.join
end

def invalid
mutations.each_with_object([]) do |mutation, aggregate|
validation = Unparser::Validation.from_node(mutation.node)
aggregate << prefix('[invalid-mutation]', validation.report) unless validation.success?
def invalid_report
invalid.map do |validation|
prefix('[invalid-mutation]', validation.report)
end
end
memoize :invalid
memoize :invalid_report

def unexpected
mutations.reject do |mutation|
valid.reject do |mutation|
example.expected.any? { |expected| expected.node.eql?(mutation.node) }
end
end
memoize :unexpected

def missing
(example.expected.map(&:node) - mutations.map(&:node)).map do |node|
Mutation::Evil.new(subject: example, node:)
example.expected.each_with_object([]) do |expected, aggregate|
next if valid.any? { |mutation| expected.node.eql?(mutation.node) }
aggregate << Mutation::Evil.new(
node: expected.node,
source: expected.original_source,
subject: example
)
end
end
memoize :missing

def no_diffs
mutations.select { |mutation| mutation.source.eql?(example.original_source_generated) }
valid.select { |mutation| mutation.source.eql?(example.source) }
end
memoize :no_diffs

Expand Down
Loading
Loading