diff --git a/ruby/lib/mutant/ast/pattern/lexer.rb b/ruby/lib/mutant/ast/pattern/lexer.rb index b27fe0bcc..4d98f468b 100644 --- a/ruby/lib/mutant/ast/pattern/lexer.rb +++ b/ruby/lib/mutant/ast/pattern/lexer.rb @@ -26,7 +26,7 @@ class Error class InvalidToken < self def display_message - <<~MESSAGE.strip + <<~MESSAGE.rstrip Invalid #{token.type} token: #{token.display_location} MESSAGE diff --git a/ruby/lib/mutant/ast/pattern/parser.rb b/ruby/lib/mutant/ast/pattern/parser.rb index 58523b079..e5386628a 100644 --- a/ruby/lib/mutant/ast/pattern/parser.rb +++ b/ruby/lib/mutant/ast/pattern/parser.rb @@ -144,7 +144,6 @@ def optional(type) return unless token&.type.equal?(type) advance_position - token end def parse_node_type diff --git a/ruby/lib/mutant/integration/rspec.rb b/ruby/lib/mutant/integration/rspec.rb index f9537ce8e..1e9a2f0c8 100644 --- a/ruby/lib/mutant/integration/rspec.rb +++ b/ruby/lib/mutant/integration/rspec.rb @@ -47,7 +47,6 @@ def setup @setup_elapsed = timer.elapsed do @runner.setup(world.stderr, world.stdout) fail 'RSpec setup failure' if rspec_setup_failure? - example_group_map end @runner.configuration.force(color_mode: :on) @runner.configuration.reporter diff --git a/ruby/lib/mutant/matcher/method.rb b/ruby/lib/mutant/matcher/method.rb index 018b7131a..27dd051a1 100644 --- a/ruby/lib/mutant/matcher/method.rb +++ b/ruby/lib/mutant/matcher/method.rb @@ -143,8 +143,6 @@ def subject_config(node) end def matched_view - return if source_location.nil? - # This is a performance optimization when using --since to avoid the cost of parsing # every source file that could possibly map to a subject. A more fine-grained filtering # takes places later in the process. diff --git a/ruby/lib/mutant/meta/example/verification.rb b/ruby/lib/mutant/meta/example/verification.rb index b7decf5c1..0ea84b937 100644 --- a/ruby/lib/mutant/meta/example/verification.rb +++ b/ruby/lib/mutant/meta/example/verification.rb @@ -118,24 +118,6 @@ def no_diffs end memoize :no_diffs - def format_mutations(mutations) - mutations.map do |mutation| - { - 'node' => mutation.node.inspect, - 'source' => mutation.source - } - end - end - - def no_diff_report - no_diffs.map do |mutation| - { - 'node' => mutation.node.inspect, - 'source' => mutation.source - } - end - end - end # Verification end # Example end # Meta diff --git a/ruby/lib/mutant/mutator/node.rb b/ruby/lib/mutant/mutator/node.rb index 69e3a1b01..5d40846ed 100644 --- a/ruby/lib/mutant/mutator/node.rb +++ b/ruby/lib/mutant/mutator/node.rb @@ -116,13 +116,6 @@ def children_indices(range) range.begin.upto(children.length + range.end) end - def mutate_single_child - children.each_with_index do |child, index| - mutate_child(index) - yield child, index unless children.one? - end - end - def run(mutator) mutator.call( config:, diff --git a/ruby/lib/mutant/mutator/node/arguments.rb b/ruby/lib/mutant/mutator/node/arguments.rb index 528eb928d..dad51a0c0 100644 --- a/ruby/lib/mutant/mutator/node/arguments.rb +++ b/ruby/lib/mutant/mutator/node/arguments.rb @@ -63,7 +63,10 @@ def emit_procarg0_removal return unless children.one? && n_procarg0?(procarg0 = Mutant::Util.one(children)) arguments = procarg0.children - emit_type(*arguments) if arguments.count > 1 + + return if arguments.one? + + emit_type(*arguments) end end # Arguments diff --git a/ruby/lib/mutant/mutator/node/case.rb b/ruby/lib/mutant/mutator/node/case.rb index f181c6b33..ba6e16aaf 100644 --- a/ruby/lib/mutant/mutator/node/case.rb +++ b/ruby/lib/mutant/mutator/node/case.rb @@ -30,9 +30,8 @@ def emit_when_mutations end def emit_else_mutations - else_branch = children.last else_index = children.length - 1 - return unless else_branch + return unless children.fetch(else_index) mutate_child(else_index) emit_child_update(else_index, nil) end diff --git a/ruby/lib/mutant/mutator/node/index.rb b/ruby/lib/mutant/mutator/node/index.rb index 3841d1272..dc9b5a922 100644 --- a/ruby/lib/mutant/mutator/node/index.rb +++ b/ruby/lib/mutant/mutator/node/index.rb @@ -33,9 +33,13 @@ def emit_send_forms end def emit_drop_mutation - return unless indices.one? && n_irange?(Mutant::Util.one(indices)) + return unless indices.one? - start, ending = *indices.first + range = Mutant::Util.one(indices) + + return unless n_irange?(range) + + start, ending = *range return unless ending.eql?(s(:int, -1)) diff --git a/ruby/lib/mutant/mutator/node/kwargs.rb b/ruby/lib/mutant/mutator/node/kwargs.rb index e4addb5e8..eda4022a5 100644 --- a/ruby/lib/mutant/mutator/node/kwargs.rb +++ b/ruby/lib/mutant/mutator/node/kwargs.rb @@ -6,10 +6,6 @@ class Node # Mutator for kwargs node class Kwargs < self - DISALLOW = %i[nil self].freeze - - private_constant(*constants(false)) - handle(:kwargs) private @@ -26,18 +22,10 @@ def emit_argument_presence end def emit_argument_mutations - children.each_with_index do |child, index| - mutate(node: child).each do |mutant| - unless forbid_argument?(mutant) - emit_child_update(index, mutant) - end - end + children.each_with_index do |_child, index| + mutate_child(index) end end - - def forbid_argument?(node) - n_pair?(node) && DISALLOW.include?(node.children.first.type) - end end # Kwargs end # Node end # Mutator diff --git a/ruby/lib/mutant/mutator/node/send.rb b/ruby/lib/mutant/mutator/node/send.rb index 132fa8690..16f4b518a 100644 --- a/ruby/lib/mutant/mutator/node/send.rb +++ b/ruby/lib/mutant/mutator/node/send.rb @@ -164,7 +164,7 @@ def emit_receiver_selector_mutations RECEIVER_SELECTOR_REPLACEMENTS .fetch(receiver.children.last, EMPTY_HASH) .fetch(selector, EMPTY_ARRAY) - .each(&method(:emit_selector)) + .each(&public_method(:emit_selector)) end def emit_double_negation_mutation diff --git a/ruby/lib/mutant/result.rb b/ruby/lib/mutant/result.rb index e80dc1cc7..99b9f6c3d 100644 --- a/ruby/lib/mutant/result.rb +++ b/ruby/lib/mutant/result.rb @@ -35,7 +35,7 @@ module ClassMethods # @return [undefined] def sum(name, collection) define_method(name) do - public_send(collection).map(&name).reduce(0, :+) + public_send(collection).map(&name).sum end memoize(name) end diff --git a/ruby/lib/mutant/scope.rb b/ruby/lib/mutant/scope.rb index d8acb0084..8959e9843 100644 --- a/ruby/lib/mutant/scope.rb +++ b/ruby/lib/mutant/scope.rb @@ -7,17 +7,6 @@ class Scope NAMESPACE_DELIMITER = '::' - # Nesting of scope - # - # @return [Enumerable] - def nesting - const = Object - name_nesting.map do |name| - const = const.const_get(name) - end - end - memoize :nesting - # Unqualified name of scope # # @return [String] diff --git a/ruby/lib/mutant/test/runner/sink.rb b/ruby/lib/mutant/test/runner/sink.rb index 9c9852755..452986173 100644 --- a/ruby/lib/mutant/test/runner/sink.rb +++ b/ruby/lib/mutant/test/runner/sink.rb @@ -22,7 +22,7 @@ def status Result::TestEnv.new( env:, runtime: env.world.timer.now - @start, - test_results: @test_results.sort_by!(&:job_index) + test_results: @test_results.sort_by(&:job_index) ) end diff --git a/ruby/meta/def.rb b/ruby/meta/def.rb index 2a17a255d..b3aa6a2ff 100644 --- a/ruby/meta/def.rb +++ b/ruby/meta/def.rb @@ -109,6 +109,15 @@ mutation 'def foo(a = true); super; end' end +Mutant::Meta::Example.add :def do + source 'def foo(_a = true); end' + + mutation 'def foo(_a); end' + mutation 'def foo(_a = false); end' + mutation 'def foo(_a = true); raise; end' + mutation 'def foo(_a = true); super; end' +end + Mutant::Meta::Example.add :def do source 'def self.foo; true; false; end' diff --git a/ruby/meta/send.rb b/ruby/meta/send.rb index f6e8c93b7..9239f0ce3 100644 --- a/ruby/meta/send.rb +++ b/ruby/meta/send.rb @@ -1,5 +1,27 @@ # frozen_string_literal: true +# Date.parse receiver selector replacements +Mutant::Meta::Example.add :send do + source 'Date.parse(foo)' + + singleton_mutations + mutation 'Date.jd(foo)' + mutation 'Date.civil(foo)' + mutation 'Date.strptime(foo)' + mutation 'Date.iso8601(foo)' + mutation 'Date.rfc3339(foo)' + mutation 'Date.xmlschema(foo)' + mutation 'Date.rfc2822(foo)' + mutation 'Date.rfc822(foo)' + mutation 'Date.httpdate(foo)' + mutation 'Date.jisx0301(foo)' + mutation 'Date.parse' + mutation 'Date.parse(nil)' + mutation 'self.parse(foo)' + mutation 'Date' + mutation 'foo' +end + Mutant::Meta::Example.add :send do source 'a > b' diff --git a/ruby/mutant.yml b/ruby/mutant.yml index bafc02a01..ffbc64fe2 100644 --- a/ruby/mutant.yml +++ b/ruby/mutant.yml @@ -24,3 +24,9 @@ matcher: - Mutant::Mutator::Node::Arguments#emit_argument_presence - Mutant::Mutator::Node::Arguments#removed_block_arg? - Mutant::Mutator::Node::BlockPass#dispatch + # mutation crashes killfork in zombie mode; Timer#now is used by mutant infrastructure + - Mutant::Timer#now + # unkillable: mutate_child on arg nodes within procarg0 produces no observable mutations + - Mutant::Mutator::Node::ProcargZero#dispatch # 5 alive + # unkillable: Zombifier is mutant infrastructure in zombie mode; mutations corrupt zombification, crashing the killfork + - Mutant::Zombifier#include? # 9 alive diff --git a/ruby/spec/unit/mutant/integration/rspec_spec.rb b/ruby/spec/unit/mutant/integration/rspec_spec.rb index bdfec9f63..5d3ee3f1b 100644 --- a/ruby/spec/unit/mutant/integration/rspec_spec.rb +++ b/ruby/spec/unit/mutant/integration/rspec_spec.rb @@ -227,6 +227,28 @@ it 'freezes object' do expect { subject }.to change { object.frozen? }.from(false).to(true) end + + it 'measures setup time' do + subject + expect(world.timer).to have_received(:elapsed) + end + + it 'forces color mode on' do + subject + expect(rspec_configuration).to have_received(:force).with(color_mode: :on) + end + + it 'accesses reporter' do + subject + expect(rspec_configuration).to have_received(:reporter) + end + + it 'resets examples' do + subject + filtered_examples.each_value do |examples| + expect(examples).to be_empty + end + end end context 'on success' do diff --git a/ruby/spec/unit/mutant/parallel/driver_spec.rb b/ruby/spec/unit/mutant/parallel/driver_spec.rb index 31a019589..cded402db 100644 --- a/ruby/spec/unit/mutant/parallel/driver_spec.rb +++ b/ruby/spec/unit/mutant/parallel/driver_spec.rb @@ -198,6 +198,22 @@ def apply include_examples 'returns expected status' end + context 'when partially done' do + before do + allow(thread_a).to receive_messages(alive?: false) + end + + let(:expected_status) do + Mutant::Parallel::Status.new( + active_jobs:, + done: false, + payload: sink_status + ) + end + + include_examples 'returns expected status' + end + include_examples 'when done' end end diff --git a/ruby/spec/unit/mutant/reporter/cli/format/progressive_spec.rb b/ruby/spec/unit/mutant/reporter/cli/format/progressive_spec.rb index 137d82c5d..77ff5c57d 100644 --- a/ruby/spec/unit/mutant/reporter/cli/format/progressive_spec.rb +++ b/ruby/spec/unit/mutant/reporter/cli/format/progressive_spec.rb @@ -29,6 +29,37 @@ ) end + describe '#start' do + let(:tty?) { false } + + subject { format.start(env) } + + it 'returns env report using Printer::Env' do + expect(subject).to include('Mutant environment:') + expect(subject).to include('Subjects:') + expect(subject).to include('Mutations:') + end + + it 'passes env to the printer' do + expect(subject).to include("Subjects: #{env.subjects.length}") + end + end + + describe '#test_start' do + let(:tty?) { false } + + subject { format.test_start(env) } + + it 'returns test env report using Printer::Test::Env' do + expect(subject).to include('Test environment:') + expect(subject).to include('Integration:') + end + + it 'passes env to the printer' do + expect(subject).to include("Tests: #{env.integration.all_tests.length}") + end + end + describe '#progress' do subject { format.progress(status) } diff --git a/ruby/spec/unit/mutant/reporter/cli/printer/config_spec.rb b/ruby/spec/unit/mutant/reporter/cli/printer/config_spec.rb index 0f39b0f82..742eaaa08 100644 --- a/ruby/spec/unit/mutant/reporter/cli/printer/config_spec.rb +++ b/ruby/spec/unit/mutant/reporter/cli/printer/config_spec.rb @@ -87,4 +87,20 @@ REPORT end end + + context 'on absent mutation timeout' do + let(:reportable) { config.with(mutation: config.mutation.with(timeout: nil)) } + + describe '.call' do + it_reports(<<~'REPORT') + Usage: unknown + Matcher: # + Integration: null + Jobs: auto + Includes: [] + Requires: [] + Operators: light + REPORT + end + end end diff --git a/ruby/spec/unit/mutant/require_highjack_spec.rb b/ruby/spec/unit/mutant/require_highjack_spec.rb index 7bf8cf5fd..d650ea32d 100644 --- a/ruby/spec/unit/mutant/require_highjack_spec.rb +++ b/ruby/spec/unit/mutant/require_highjack_spec.rb @@ -65,5 +65,18 @@ def apply .not_to change { require_calls }.from([]) end end + + context 'with native-style method definitions' do + let(:target_module) do + Module.new.tap do |mod| + mod.class_eval('def require(x); end', __FILE__, __LINE__) + mod.class_eval('def self.require(x); end', __FILE__, __LINE__) + end + end + + it 'does not emit method redefinition warnings' do + expect { apply }.not_to output.to_stderr + end + end end end diff --git a/ruby/spec/unit/mutant/scope_spec.rb b/ruby/spec/unit/mutant/scope_spec.rb index 780a859be..9715f8665 100644 --- a/ruby/spec/unit/mutant/scope_spec.rb +++ b/ruby/spec/unit/mutant/scope_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true RSpec.describe Mutant::Scope do + let(:object) do + Mutant::Scope.new( + expression: instance_double(Mutant::Expression), + raw: raw_scope + ) + end + describe '#unqualified_name' do subject { object.unqualified_name } - let(:object) do - Mutant::Scope.new( - expression: instance_double(Mutant::Expression), - raw: raw_scope - ) - end - context 'with top level constant name' do let(:raw_scope) { TestApp } @@ -31,4 +31,53 @@ it_behaves_like 'an idempotent method' end end + + describe '#match_expressions' do + subject { object.match_expressions } + + def recursive(scope_name) + Mutant::Expression::Namespace::Recursive.new(scope_name:) + end + + context 'with top level constant name' do + let(:raw_scope) { TestApp } + + it 'returns single recursive expression' do + is_expected.to eql([recursive('TestApp')]) + end + + it_behaves_like 'an idempotent method' + end + + context 'with two level constant name' do + let(:raw_scope) { TestApp::Literal } + + it 'returns expressions from most to least specific' do + is_expected.to eql( + [ + recursive('TestApp::Literal'), + recursive('TestApp') + ] + ) + end + + it_behaves_like 'an idempotent method' + end + + context 'with three level constant name' do + let(:raw_scope) { double('scope', name: 'TestApp::Literal::Deep') } + + it 'returns expressions from most to least specific' do + is_expected.to eql( + [ + recursive('TestApp::Literal::Deep'), + recursive('TestApp::Literal'), + recursive('TestApp') + ] + ) + end + + it_behaves_like 'an idempotent method' + end + end end diff --git a/ruby/spec/unit/mutant/world_spec.rb b/ruby/spec/unit/mutant/world_spec.rb index 80fb77938..acff1381a 100644 --- a/ruby/spec/unit/mutant/world_spec.rb +++ b/ruby/spec/unit/mutant/world_spec.rb @@ -81,7 +81,9 @@ def apply end before do - allow(open3).to receive_messages(capture3: [stdout, stderr, process_status]) + allow(open3).to receive(:capture3) + .with(*command, binmode: true) + .and_return([stdout, stderr, process_status]) end context 'when process exists successful' do