Skip to content

Commit

Permalink
Upgrade to unparser 0.7.x interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Feb 2, 2025
1 parent 764bb20 commit 221af35
Show file tree
Hide file tree
Showing 65 changed files with 869 additions and 373 deletions.
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'
22 changes: 17 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 @@ -73,15 +76,24 @@ def add_target_options(parser)
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
50 changes: 33 additions & 17 deletions lib/mutant/meta/example/verification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,29 @@ module Meta
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]
#
# mutant:disable
def success?
[
original_verification,
invalid,
invalid_report,
missing,
no_diffs,
original_verification_report,
unexpected
].all?(&:empty?)
end
Expand All @@ -29,12 +41,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 +64,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 +89,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

0 comments on commit 221af35

Please sign in to comment.