├── ruby ├── Gemfile.shared ├── test_app │ ├── .rspec │ ├── simple.rb │ ├── spec │ │ ├── unit │ │ │ └── test_app │ │ │ │ ├── invalid.rb │ │ │ │ └── literal_spec.rb │ │ └── spec_helper.rb │ ├── Gemfile.minitest │ ├── Gemfile.rspec3.12 │ ├── Gemfile.rspec3.8 │ ├── Gemfile.rspec3.9 │ ├── Gemfile.rspec3.10 │ ├── Gemfile.rspec3.11 │ ├── Gemfile.rspec3.13 │ ├── test │ │ └── unit │ │ │ └── test_app │ │ │ └── literal_test.rb │ ├── lib │ │ └── test_app │ │ │ └── literal.rb │ └── Gemfile.minitest.lock ├── .rspec ├── meta │ ├── file.rb │ ├── line.rb │ ├── nil.rb │ ├── redo.rb │ ├── cvar.rb │ ├── false.rb │ ├── gvar.rb │ ├── self.rb │ ├── str.rb │ ├── true.rb │ ├── cvasgn.rb │ ├── gvasgn.rb │ ├── cbase.rb │ ├── ivar.rb │ ├── lvar.rb │ ├── sym.rb │ ├── dstr.rb │ ├── kwbegin.rb │ ├── xstr.rb │ ├── dsym.rb │ ├── regexp │ │ ├── regexp_eos_anchor.rb │ │ ├── regexp_latin_property.rb │ │ ├── regexp_bol_anchor.rb │ │ ├── regexp_eol_anchor.rb │ │ ├── regexp_root_expression.rb │ │ ├── regexp_eos_ob_eol_anchor.rb │ │ ├── regexp_alternation_meta.rb │ │ ├── regexp_bos_anchor.rb │ │ ├── regexp_capture_group.rb │ │ ├── regexp_named_group.rb │ │ ├── regexp_zero_or_more.rb │ │ └── character_types.rb │ ├── const_pattern.rb │ ├── and_asgn.rb │ ├── break.rb │ ├── yield.rb │ ├── kwarg.rb │ ├── ensure.rb │ ├── masgn.rb │ ├── blockarg.rb │ ├── next.rb │ ├── regopt.rb │ ├── numblock.rb │ ├── class.rb │ ├── and.rb │ ├── module.rb │ ├── nthref.rb │ ├── sclass.rb │ ├── int.rb │ ├── kwoptarg.rb │ ├── return.rb │ ├── defined.rb │ ├── ivasgn.rb │ ├── const.rb │ ├── lvasgn.rb │ ├── match_current_line.rb │ ├── until.rb │ ├── super.rb │ ├── lambda.rb │ ├── casgn.rb │ ├── array.rb │ ├── csend.rb │ ├── hash.rb │ ├── while.rb │ ├── indexasgn.rb │ ├── op_assgn.rb │ ├── float.rb │ ├── or_asgn.rb │ ├── procarg_zero.rb │ ├── block_pass.rb │ └── range.rb ├── lib │ └── mutant │ │ ├── repository.rb │ │ ├── ast │ │ ├── meta.rb │ │ ├── pattern │ │ │ ├── token.rb │ │ │ └── source.rb │ │ ├── meta │ │ │ ├── symbol.rb │ │ │ ├── resbody.rb │ │ │ ├── const.rb │ │ │ └── optarg.rb │ │ ├── node_predicates.rb │ │ ├── sexp.rb │ │ └── nodes.rb │ │ ├── mutator │ │ ├── util.rb │ │ ├── node │ │ │ ├── mlhs.rb │ │ │ ├── const_pattern.rb │ │ │ ├── module.rb │ │ │ ├── procarg_zero.rb │ │ │ ├── literal │ │ │ │ ├── nil.rb │ │ │ │ ├── string.rb │ │ │ │ ├── symbol.rb │ │ │ │ ├── boolean.rb │ │ │ │ ├── integer.rb │ │ │ │ ├── float.rb │ │ │ │ ├── range.rb │ │ │ │ ├── array.rb │ │ │ │ ├── hash.rb │ │ │ │ └── regex.rb │ │ │ ├── zsuper.rb │ │ │ ├── kwbegin.rb │ │ │ ├── literal.rb │ │ │ ├── noop.rb │ │ │ ├── class.rb │ │ │ ├── numblock.rb │ │ │ ├── resbody.rb │ │ │ ├── masgn.rb │ │ │ ├── sclass.rb │ │ │ ├── send │ │ │ │ ├── conditional.rb │ │ │ │ ├── attribute_assignment.rb │ │ │ │ └── binary.rb │ │ │ ├── yield.rb │ │ │ ├── and_asgn.rb │ │ │ ├── generic.rb │ │ │ ├── break.rb │ │ │ ├── splat.rb │ │ │ ├── next.rb │ │ │ ├── match_current_line.rb │ │ │ ├── return.rb │ │ │ ├── nthref.rb │ │ │ ├── super.rb │ │ │ ├── dynamic_literal.rb │ │ │ ├── regopt.rb │ │ │ ├── conditional_loop.rb │ │ │ ├── or_asgn.rb │ │ │ ├── const.rb │ │ │ ├── block_pass.rb │ │ │ ├── defined.rb │ │ │ ├── op_asgn.rb │ │ │ ├── named_value │ │ │ │ ├── variable_assignment.rb │ │ │ │ ├── constant_assignment.rb │ │ │ │ └── access.rb │ │ │ ├── argument.rb │ │ │ ├── binary.rb │ │ │ ├── when.rb │ │ │ ├── case.rb │ │ │ ├── kwargs.rb │ │ │ └── rescue.rb │ │ └── util │ │ │ ├── symbol.rb │ │ │ └── array.rb │ │ ├── cli.rb │ │ ├── range.rb │ │ ├── selector.rb │ │ ├── test.rb │ │ ├── reporter │ │ ├── null.rb │ │ ├── cli │ │ │ └── printer │ │ │ │ ├── coverage_result.rb │ │ │ │ ├── env_result.rb │ │ │ │ ├── subject_result.rb │ │ │ │ ├── config.rb │ │ │ │ ├── status_progressive.rb │ │ │ │ └── env.rb │ │ └── sequence.rb │ │ ├── matcher │ │ ├── static.rb │ │ ├── null.rb │ │ ├── filter.rb │ │ ├── chain.rb │ │ ├── descendants.rb │ │ ├── namespace.rb │ │ └── scope.rb │ │ ├── selector │ │ ├── null.rb │ │ └── expression.rb │ │ ├── procto.rb │ │ ├── version.rb │ │ ├── expression │ │ ├── descendants.rb │ │ ├── parser.rb │ │ └── source.rb │ │ ├── segment.rb │ │ ├── cli │ │ └── command │ │ │ ├── environment │ │ │ ├── irb.rb │ │ │ ├── show.rb │ │ │ └── subject.rb │ │ │ └── root.rb │ │ ├── isolation │ │ ├── exception.rb │ │ └── none.rb │ │ ├── subject │ │ ├── method │ │ │ ├── singleton.rb │ │ │ ├── metaclass.rb │ │ │ └── instance.rb │ │ ├── config.rb │ │ └── method.rb │ │ ├── integration │ │ └── null.rb │ │ ├── require_highjack.rb │ │ ├── isolation.rb │ │ ├── parser.rb │ │ ├── util.rb │ │ ├── mutator.rb │ │ ├── parallel │ │ └── pipe.rb │ │ ├── ast.rb │ │ ├── meta.rb │ │ ├── registry.rb │ │ ├── loader.rb │ │ ├── scope.rb │ │ ├── repository │ │ └── diff │ │ │ └── ranges.rb │ │ ├── context.rb │ │ ├── minitest │ │ └── coverage.rb │ │ ├── reporter.rb │ │ ├── test │ │ └── runner │ │ │ └── sink.rb │ │ └── expression.rb ├── Gemfile ├── spec │ ├── unit │ │ ├── mutant │ │ │ ├── ast │ │ │ │ ├── pattern │ │ │ │ │ ├── any_spec.rb │ │ │ │ │ ├── none_spec.rb │ │ │ │ │ ├── source │ │ │ │ │ │ └── location_spec.rb │ │ │ │ │ └── source_spec.rb │ │ │ │ ├── structure_spec.rb │ │ │ │ ├── meta │ │ │ │ │ ├── optarg_spec.rb │ │ │ │ │ └── send │ │ │ │ │ │ ├── receiver_possible_top_level_const_predicate_spec.rb │ │ │ │ │ │ └── proc_predicate_spec.rb │ │ │ │ └── sexp_spec.rb │ │ │ ├── matcher │ │ │ │ ├── null_spec.rb │ │ │ │ ├── static_spec.rb │ │ │ │ ├── filter_spec.rb │ │ │ │ └── chain_spec.rb │ │ │ ├── result │ │ │ │ ├── test_spec.rb │ │ │ │ └── coverage_criteria_spec.rb │ │ │ ├── selector │ │ │ │ └── null_spec.rb │ │ │ ├── reporter │ │ │ │ ├── null_spec.rb │ │ │ │ ├── cli │ │ │ │ │ └── printer │ │ │ │ │ │ ├── test │ │ │ │ │ │ ├── result_spec.rb │ │ │ │ │ │ ├── status_progressive_spec.rb │ │ │ │ │ │ └── config_spec.rb │ │ │ │ │ │ ├── subject_result_spec.rb │ │ │ │ │ │ ├── env_result_spec.rb │ │ │ │ │ │ └── status_progressive_spec.rb │ │ │ │ └── sequence_spec.rb │ │ │ ├── procto_spec.rb │ │ │ ├── transform │ │ │ │ ├── success_spec.rb │ │ │ │ ├── exception_spec.rb │ │ │ │ ├── named_spec.rb │ │ │ │ ├── primitive_spec.rb │ │ │ │ ├── block_spec.rb │ │ │ │ └── bool_spec.rb │ │ │ ├── result_spec.rb │ │ │ ├── segment_spec.rb │ │ │ ├── scope_spec.rb │ │ │ ├── isolation │ │ │ │ └── none_spec.rb │ │ │ ├── integration │ │ │ │ └── config_spec.rb │ │ │ ├── expression │ │ │ │ ├── descendants_spec.rb │ │ │ │ └── namespace │ │ │ │ │ └── exact_spec.rb │ │ │ ├── mutator │ │ │ │ └── regexp │ │ │ │ │ └── registry_spec.rb │ │ │ ├── expression_spec.rb │ │ │ ├── ast_spec.rb │ │ │ ├── timer_spec.rb │ │ │ ├── parallel │ │ │ │ └── source │ │ │ │ │ └── array_spec.rb │ │ │ ├── parser_spec.rb │ │ │ ├── config │ │ │ │ └── coverage_criteria_spec.rb │ │ │ └── mutation │ │ │ │ └── generation_error_spec.rb │ │ └── mutant_spec.rb │ ├── integration │ │ └── mutant │ │ │ ├── test_mutator_handles_types_spec.rb │ │ │ ├── null_spec.rb │ │ │ ├── minitest_spec.rb │ │ │ ├── corpus_spec.rb │ │ │ ├── rspec_spec.rb │ │ │ └── isolation │ │ │ └── fork_spec.rb │ ├── shared │ │ ├── config_merge_behavior.rb │ │ └── method_matcher_behavior.rb │ └── support │ │ └── file_system.rb ├── scripts │ └── devloop.sh ├── bin │ └── mutant ├── mutant.yml ├── mutant-rspec.gemspec ├── mutant-minitest.gemspec └── mutant.gemspec ├── .gitignore ├── Cargo.toml ├── rust-toolchain.toml ├── manager └── Cargo.toml ├── docs ├── commercial-support.md ├── limitations.md └── sorbet.md └── mutant ├── Cargo.toml └── src └── main.rs /ruby/Gemfile.shared: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ruby/test_app/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /ruby/test_app/simple.rb: -------------------------------------------------------------------------------- 1 | true 2 | -------------------------------------------------------------------------------- /ruby/test_app/spec/unit/test_app/invalid.rb: -------------------------------------------------------------------------------- 1 | fail 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /Cargo.lock 3 | /target 4 | /tmp 5 | /ruby/LICENSE 6 | /ruby/VERSION 7 | -------------------------------------------------------------------------------- /ruby/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format progress 4 | --warnings 5 | --order random 6 | -------------------------------------------------------------------------------- /ruby/meta/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add do 4 | source '__FILE__' 5 | end 6 | -------------------------------------------------------------------------------- /ruby/meta/line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add do 4 | source '__LINE__' 5 | end 6 | -------------------------------------------------------------------------------- /ruby/meta/nil.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :nil do 4 | source 'nil' 5 | end 6 | -------------------------------------------------------------------------------- /ruby/meta/redo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :redo do 4 | source 'redo' 5 | end 6 | -------------------------------------------------------------------------------- /ruby/lib/mutant/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module Repository 5 | end # Repository 6 | end # Mutant 7 | -------------------------------------------------------------------------------- /ruby/meta/cvar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :cvar do 4 | source '@@a' 5 | 6 | singleton_mutations 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/false.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :false do 4 | source 'false' 5 | 6 | mutation 'true' 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/gvar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :gvar do 4 | source '$a' 5 | 6 | singleton_mutations 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/self.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :self do 4 | source 'self' 5 | 6 | mutation 'nil' 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/str.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :str do 4 | source '"foo"' 5 | 6 | singleton_mutations 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/true.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :true do 4 | source 'true' 5 | 6 | mutation 'false' 7 | end 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["mutant", "manager"] 4 | 5 | [workspace.package] 6 | version = "0.13.5" 7 | edition = "2024" 8 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec name: 'mutant' 6 | 7 | eval_gemfile 'Gemfile.shared' 8 | -------------------------------------------------------------------------------- /ruby/meta/cvasgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :cvasgn do 4 | source '@@a = true' 5 | 6 | mutation '@@a = false' 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/gvasgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :gvasgn do 4 | source '$a = true' 5 | 6 | mutation '$a = false' 7 | end 8 | -------------------------------------------------------------------------------- /ruby/meta/cbase.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :cbase do 4 | source '::A' 5 | 6 | singleton_mutations 7 | mutation 'A' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/ivar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :ivar do 4 | source '@foo' 5 | 6 | singleton_mutations 7 | mutation 'foo' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/lvar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :lvar do 4 | declare_lvar :a 5 | 6 | source 'a' 7 | 8 | mutation 'nil' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/sym.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :sym do 4 | source ':foo' 5 | 6 | singleton_mutations 7 | mutation ':foo__mutant__' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/dstr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :dstr do 4 | source '"foo#{bar}baz"' 5 | 6 | singleton_mutations 7 | mutation '"foo#{nil}baz"' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/kwbegin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :kwbegin do 4 | source 'begin; true; end' 5 | 6 | singleton_mutations 7 | mutation 'begin; false; end' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/xstr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :xstr do 4 | source '`a #{true}`' 5 | 6 | singleton_mutations 7 | 8 | mutation '`a #{false}`' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/dsym.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :dsym do 4 | source ':"foo#{bar}baz"' 5 | 6 | singleton_mutations 7 | 8 | mutation ':"foo#{nil}baz"' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_eos_anchor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_eos_anchor do 4 | source '/\z/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/const_pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :const_pattern do 4 | source <<~'RUBY' 5 | case nil 6 | in A(foo, bar) 7 | end 8 | RUBY 9 | end 10 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/meta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Node meta information mixin 6 | module Meta 7 | end # Meta 8 | end # AST 9 | end # Mutant 10 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.minitest: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'mutant', path: '../' 4 | gem 'mutant-minitest', path: '../' 5 | 6 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 7 | -------------------------------------------------------------------------------- /ruby/meta/and_asgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :and_asgn do 4 | source 'a &&= 1' 5 | 6 | mutation 'a &&= nil' 7 | mutation 'a &&= 0' 8 | mutation 'a &&= 2' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/break.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :break do 4 | source 'break true' 5 | 6 | singleton_mutations 7 | mutation 'break false' 8 | mutation 'break' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/yield.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :yield do 4 | source 'yield true' 5 | 6 | singleton_mutations 7 | mutation 'yield false' 8 | mutation 'yield' 9 | end 10 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.91.1" 3 | targets = [ 4 | "aarch64-unknown-linux-musl", 5 | "aarch64-unknown-linux-gnu", 6 | "x86_64-unknown-linux-musl", 7 | ] 8 | components = ["clippy", "rustfmt"] 9 | -------------------------------------------------------------------------------- /ruby/meta/kwarg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :kwarg do 4 | source 'def foo(bar:); end' 5 | 6 | mutation 'def foo(bar:); raise; end' 7 | mutation 'def foo(bar:); super; end' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/ensure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :ensure do 4 | source 'begin; rescue; ensure; true; end' 5 | 6 | singleton_mutations 7 | mutation 'begin; rescue; ensure; false; end' 8 | end 9 | -------------------------------------------------------------------------------- /ruby/meta/masgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :masgn do 4 | source 'a, b = nil, nil' 5 | 6 | mutation 'a, b = nil' 7 | mutation 'a, b = []' 8 | mutation 'a, b = [nil]' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_latin_property.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp, :regexp_latin_property do 4 | source('/p{latin}/') 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | end 9 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | # Namespace for utility mutators 6 | class Util < self 7 | end # Util 8 | end # Mutator 9 | end # Mutant 10 | -------------------------------------------------------------------------------- /ruby/meta/blockarg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :blockarg do 4 | source 'foo { |&bar| }' 5 | 6 | singleton_mutations 7 | mutation 'foo { |&bar| raise }' 8 | mutation 'foo' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/next.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :next do 4 | source 'next true' 5 | 6 | singleton_mutations 7 | mutation 'next false' 8 | mutation 'next' 9 | mutation 'break true' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_bol_anchor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_bol_anchor do 4 | source '/^/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\\A/' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_eol_anchor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_eol_anchor do 4 | source '/$/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\z/' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_root_expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_root_expression do 4 | source '/^/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\\A/' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/regopt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regopt do 4 | source '/foo/ixom' 5 | 6 | singleton_mutations 7 | mutation '//ixom' 8 | mutation '/nomatch\\A/ixom' 9 | mutation '/foo/xom' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/numblock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :numblock do 4 | source 'foo(false) { _1.to_s }' 5 | 6 | singleton_mutations 7 | mutation 'foo(true) { _1.to_s }' 8 | mutation 'foo { _1.to_s }' 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_eos_ob_eol_anchor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_eos_ob_eol_anchor do 4 | source '/\Z/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\z/' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :class do 4 | source 'class Foo; bar; end' 5 | 6 | mutation 'class Foo; nil; end' 7 | end 8 | 9 | Mutant::Meta::Example.add :class do 10 | source 'class Foo; end' 11 | end 12 | -------------------------------------------------------------------------------- /ruby/meta/and.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :and do 4 | source 'true and false' 5 | 6 | singleton_mutations 7 | mutation 'true' 8 | mutation 'false' 9 | mutation 'false and false' 10 | mutation 'true and true' 11 | end 12 | -------------------------------------------------------------------------------- /ruby/meta/module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :module do 4 | source 'module Foo; bar; end' 5 | 6 | mutation 'module Foo; nil; end' 7 | end 8 | 9 | Mutant::Meta::Example.add :module do 10 | source 'module Foo; end' 11 | end 12 | -------------------------------------------------------------------------------- /ruby/meta/nthref.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :nth_ref do 4 | source '$1' 5 | 6 | mutation '$2' 7 | end 8 | 9 | Mutant::Meta::Example.add :nth_ref do 10 | source '$3' 11 | 12 | mutation '$2' 13 | mutation '$4' 14 | end 15 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/pattern/any_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Pattern::Any do 4 | describe '#match?' do 5 | it 'returns true' do 6 | expect(subject.match?(s(:true))).to be(true) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/pattern/none_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Pattern::None do 4 | describe '#match?' do 5 | it 'returns true' do 6 | expect(subject.match?(s(:true))).to be(false) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.12: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.9' 3 | gem 'rspec-core', '~> 3.9' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.8: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.8' 3 | gem 'rspec-core', '~> 3.8' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.9: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.9' 3 | gem 'rspec-core', '~> 3.9' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/meta/sclass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :sclass do 4 | source 'class << self; bar; end' 5 | 6 | mutation 'class << self; nil; end' 7 | end 8 | 9 | Mutant::Meta::Example.add :sclass do 10 | source 'class << self; end' 11 | end 12 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.10: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.10' 3 | gem 'rspec-core', '~> 3.10' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.11: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.12' 3 | gem 'rspec-core', '~> 3.12' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.rspec3.13: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rspec', '~> 3.13' 3 | gem 'rspec-core', '~> 3.13' 4 | gem 'mutant', path: '../' 5 | gem 'mutant-rspec', path: '../' 6 | gem 'adamantium' 7 | eval_gemfile File.expand_path('../../Gemfile.shared', __FILE__) 8 | -------------------------------------------------------------------------------- /ruby/meta/int.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :int do 4 | source '10' 5 | 6 | singleton_mutations 7 | 8 | # edge cases 9 | mutation '0' 10 | mutation '1' 11 | 12 | # scalar boundary 13 | mutation '9' 14 | mutation '11' 15 | end 16 | -------------------------------------------------------------------------------- /ruby/test_app/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib') 2 | 3 | require 'test_app' 4 | require 'rspec' 5 | 6 | # require spec support files and shared behavior 7 | Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].each do |file| 8 | require file 9 | end 10 | -------------------------------------------------------------------------------- /ruby/meta/kwoptarg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :kwarg do 4 | source 'def foo(bar: baz); end' 5 | 6 | mutation 'def foo(bar: baz); raise; end' 7 | mutation 'def foo(bar: baz); super; end' 8 | mutation 'def foo(bar: nil); end' 9 | mutation 'def foo(bar:); end' 10 | end 11 | -------------------------------------------------------------------------------- /ruby/meta/return.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :return do 4 | source 'return' 5 | 6 | singleton_mutations 7 | end 8 | 9 | Mutant::Meta::Example.add :return do 10 | source 'return foo' 11 | 12 | singleton_mutations 13 | mutation 'foo' 14 | mutation 'return nil' 15 | end 16 | -------------------------------------------------------------------------------- /ruby/lib/mutant/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Commandline interface 5 | module CLI 6 | # Parse command 7 | # 8 | # @return [Command] 9 | def self.parse(arguments:, world:) 10 | Command::Root.parse(arguments:, world:) 11 | end 12 | end # CLI 13 | end # Mutant 14 | -------------------------------------------------------------------------------- /ruby/meta/defined.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :defined? do 4 | source 'defined?(foo)' 5 | 6 | mutation 'nil' 7 | end 8 | 9 | Mutant::Meta::Example.add :defined? do 10 | source 'defined?(@foo)' 11 | 12 | mutation 'instance_variable_defined?(:@foo)' 13 | mutation 'nil' 14 | end 15 | -------------------------------------------------------------------------------- /ruby/scripts/devloop.sh: -------------------------------------------------------------------------------- 1 | while inotifywait ../unparser/lib/**/*.rb lib/**/*.rb meta/**/*.rb spec/**/*.rb Gemfile Gemfile.shared mutant.gemspec; do 2 | bundle exec mutant environment test run --fail-fast spec/unit \ 3 | && bundle exec mutant run --fail-fast --since main --zombie -- 'Mutant*' \ 4 | && bundle exec rubocop 5 | done 6 | -------------------------------------------------------------------------------- /ruby/meta/ivasgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :ivasgn do 4 | source '@a = true' 5 | 6 | mutation '@a = false' 7 | end 8 | 9 | Mutant::Meta::Example.add :ivasgn do 10 | source '@a &&= 1' 11 | 12 | mutation '@a &&= nil' 13 | mutation '@a &&= 0' 14 | mutation '@a &&= 2' 15 | end 16 | -------------------------------------------------------------------------------- /ruby/meta/const.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :const do 4 | source 'A::B::C' 5 | 6 | singleton_mutations 7 | mutation 'B::C' 8 | mutation 'C' 9 | end 10 | 11 | Mutant::Meta::Example.add :const do 12 | source 'A.foo' 13 | 14 | singleton_mutations 15 | mutation 'A' 16 | mutation 'self.foo' 17 | end 18 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/matcher/null_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Matcher::Null, '#call' do 4 | let(:object) { described_class.new } 5 | let(:env) { instance_double(Mutant::Env) } 6 | 7 | subject { object.call(env) } 8 | 9 | it 'returns no subjects' do 10 | should eql([]) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ruby/bin/mutant: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Dispatcher that selects between mutant-ruby and mutant-rust 5 | # based on MUTANT_RUST environment variable. 6 | # 7 | # See RUST.md for documentation. 8 | 9 | fail 'MUTANT_RUST=1 is not yet supported via gem installation' if ENV['MUTANT_RUST'] 10 | 11 | exec('mutant-ruby', *ARGV) 12 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/test_mutator_handles_types_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'AST type coverage', mutant: false do 4 | specify 'mutant should not crash for any node parser can generate' do 5 | Mutant::AST::Types::ALL.each do |type| 6 | Mutant::Mutator::Node::REGISTRY.lookup(type) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/pattern/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | class Pattern 6 | class Token 7 | include Anima.new(:type, :value, :location) 8 | 9 | def display_location 10 | location.display 11 | end 12 | end # Token 13 | end # Pattern 14 | end # AST 15 | end # Mutant 16 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_alternation_meta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_alternation_meta do 4 | source '/\A(foo|bar|baz)\z/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\A(foo|bar)\z/' 10 | mutation '/\A(foo|baz)\z/' 11 | mutation '/\A(bar|baz)\z/' 12 | mutation '/\A(?:foo|bar|baz)\z/' 13 | end 14 | -------------------------------------------------------------------------------- /ruby/meta/lvasgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :lvasgn do 4 | source 'a = true' 5 | 6 | mutation 'a = false' 7 | end 8 | 9 | Mutant::Meta::Example.add :array, :lvasgn do 10 | source 'a = *b' 11 | 12 | mutation 'a = nil' 13 | mutation 'a = []' 14 | mutation 'a = [nil]' 15 | mutation 'a = [*nil]' 16 | mutation 'a = [b]' 17 | end 18 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_bos_anchor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_bos_anchor do 4 | source '/\A/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | end 9 | 10 | Mutant::Meta::Example.add :regexp_bos_anchor do 11 | source '/^#{a}/' 12 | 13 | singleton_mutations 14 | regexp_mutations 15 | 16 | mutation '/^#{nil}/' 17 | end 18 | -------------------------------------------------------------------------------- /ruby/lib/mutant/range.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module Range 5 | # Test if two ranges overlap 6 | # 7 | # @param [Range] left 8 | # @param [Range] right 9 | # 10 | # @return [Boolean] 11 | def self.overlap?(left, right) 12 | left.end >= right.begin && right.end >= left.begin 13 | end 14 | end # Range 15 | end # end 16 | -------------------------------------------------------------------------------- /manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manager" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Markus Schirp "] 6 | license = "Nonstandard" 7 | description = "Mutant development manager" 8 | 9 | [[bin]] 10 | name = "manager" 11 | path = "src/main.rs" 12 | 13 | [dependencies] 14 | clap = { version = "4.5", features = ["derive"] } 15 | toml = "0.8" 16 | -------------------------------------------------------------------------------- /ruby/lib/mutant/selector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Abstract base class for test selectors 5 | class Selector 6 | include AbstractType, Adamantium 7 | 8 | # Tests for subject 9 | # 10 | # @param [Subject] subjecto 11 | # 12 | # @return [Enumerable] 13 | abstract_method :call 14 | 15 | end # Selector 16 | end # Mutant 17 | -------------------------------------------------------------------------------- /ruby/lib/mutant/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Abstract base class for test that might kill a mutation 5 | class Test 6 | include Anima.new( 7 | :expressions, 8 | :id 9 | ) 10 | 11 | # Identification string 12 | # 13 | # @return [String] 14 | alias_method :identification, :id 15 | 16 | end # Test 17 | end # Mutant 18 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/mlhs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for multiple assignment left hand side nodes 7 | class MLHS < self 8 | 9 | handle(:mlhs) 10 | 11 | private 12 | 13 | def dispatch; end 14 | 15 | end # MLHS 16 | end # Node 17 | end # Mutator 18 | end # Mutant 19 | -------------------------------------------------------------------------------- /ruby/meta/match_current_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :match_current_line do 4 | source 'true if /foo/' 5 | 6 | singleton_mutations 7 | mutation 'false if /foo/' 8 | mutation 'true if //' 9 | mutation 'true if true' 10 | mutation 'true if false' 11 | mutation 'true if nil' 12 | mutation 'true if /nomatch\A/' 13 | mutation 'true' 14 | end 15 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/const_pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class ConstPattern < self 7 | handle(:const_pattern) 8 | 9 | children(:target, :pattern) 10 | 11 | private 12 | 13 | def dispatch; end 14 | 15 | end # ConstPAttern 16 | end # Node 17 | end # Mutator 18 | end # Mutant 19 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/result/test_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Result::Test::VoidValue do 4 | describe '.new' do 5 | it 'returns expected attributes' do 6 | expect(described_class.instance.to_h).to eql( 7 | job_index: nil, 8 | output: '', 9 | passed: false, 10 | runtime: 0.0 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | 6 | # Null reporter 7 | class Null < self 8 | include Equalizer.new 9 | 10 | %w[warn report start progress].each do |name| 11 | define_method name do |_object| 12 | self 13 | end 14 | end 15 | 16 | end # Null 17 | end # Reporter 18 | end # Mutant 19 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/module.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Module < self 7 | handle :module 8 | 9 | children :klass, :body 10 | 11 | private 12 | 13 | def dispatch 14 | emit_body_mutations if body 15 | end 16 | end # Module 17 | end # Node 18 | end # Mutator 19 | end # Mutant 20 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/procarg_zero.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class ProcargZero < self 7 | 8 | handle :procarg0 9 | 10 | private 11 | 12 | def dispatch 13 | children.each_index(&method(:mutate_child)) 14 | end 15 | end # ProcargZero 16 | end # Node 17 | end # Mutator 18 | end # Mutant 19 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/meta/symbol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Node meta information mixin 6 | module Meta 7 | 8 | # Metadata for symbol nodes 9 | class Symbol 10 | include NamedChildren, Anima.new(:node) 11 | 12 | children :name 13 | 14 | public :name 15 | 16 | end # Symbol 17 | end # Meta 18 | end # AST 19 | end # Mutant 20 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/nil.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal 7 | # Mutator for nil literals 8 | class Nil < self 9 | 10 | handle(:nil) 11 | 12 | private 13 | 14 | def dispatch; end 15 | 16 | end # Nil 17 | end # Literal 18 | end # Node 19 | end # Mutator 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/zsuper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for super without parentheses 8 | class ZSuper < self 9 | 10 | handle(:zsuper) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons 16 | end 17 | 18 | end # ZSuper 19 | end # Node 20 | end # Mutator 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/static.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher returning subjects already known at its creation time 6 | class Static 7 | include Anima.new(:subjects) 8 | 9 | # Call matcher 10 | # 11 | # @return [Enumerable] 12 | def call(_env) 13 | subjects 14 | end 15 | end # Static 16 | end # Matcher 17 | end # Mutant 18 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/kwbegin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Kwbegin mutator 8 | class Kwbegin < Generic 9 | 10 | handle(:kwbegin) 11 | 12 | private 13 | 14 | def dispatch 15 | super 16 | emit_singletons 17 | end 18 | 19 | end # Kwbegin 20 | end # Node 21 | end # Mutator 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/selector/null_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Selector::Null do 4 | describe '#call' do 5 | subject { described_class.new } 6 | 7 | let(:mutant_subject) { instance_double(Mutant::Subject) } 8 | 9 | def apply 10 | subject.call(mutant_subject) 11 | end 12 | 13 | it 'returns no tests' do 14 | expect(apply).to eql([]) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Abstract mutator for literal AST nodes 7 | class Literal < self 8 | include AbstractType 9 | 10 | private 11 | 12 | def emit_values 13 | values.each(&method(:emit_type)) 14 | end 15 | end # Literal 16 | end # Node 17 | end # Mutator 18 | end # Mutant 19 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/noop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutation emitter to handle noop nodes 8 | class Noop < self 9 | 10 | handle(:__ENCODING__, :cbase, :lambda) 11 | 12 | private 13 | 14 | def dispatch 15 | # noop 16 | end 17 | 18 | end # Noop 19 | end # Node 20 | end # Mutator 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/null_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::Null do 4 | let(:object) { described_class.new } 5 | let(:value) { instance_double(Object) } 6 | 7 | %i[progress report start warn].each do |name| 8 | describe "##{name}" do 9 | subject { object.public_send(name, value) } 10 | 11 | it_should_behave_like 'a command method' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/util/symbol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Util 6 | 7 | # Utility symbol mutator 8 | class Symbol < self 9 | 10 | POSTFIX = '__mutant__' 11 | 12 | private 13 | 14 | def dispatch 15 | emit((input.to_s + POSTFIX).to_sym) 16 | end 17 | 18 | end # Symbol 19 | end # Util 20 | end # Mutator 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/meta/until.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :until do 4 | source 'until true; foo; bar; end' 5 | 6 | singleton_mutations 7 | mutation 'until true; bar; end' 8 | mutation 'until true; foo; end' 9 | mutation 'until true; end' 10 | mutation 'until false; foo; bar; end' 11 | mutation 'until true; foo; nil; end' 12 | mutation 'until true; nil; bar; end' 13 | mutation 'until true; raise; end' 14 | end 15 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/null_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'null integration', mutant: false do 4 | 5 | let(:base_cmd) { 'bundle exec mutant run -I lib --require test_app "TestApp*"' } 6 | 7 | around do |example| 8 | Dir.chdir(TestApp.root) do 9 | example.run 10 | end 11 | end 12 | 13 | specify 'it allows to kill mutations' do 14 | expect(Kernel.system(base_cmd)).to be(false) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/matcher/static_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Matcher::Static, '#call' do 4 | let(:object) { described_class.new(subjects:) } 5 | let(:env) { instance_double(Mutant::Env) } 6 | let(:subjects) { instance_double(Array) } 7 | 8 | subject { object.call(env) } 9 | 10 | it 'returns its predefined subjects' do 11 | should be(subjects) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/procto_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Procto do 4 | let(:klass) do 5 | Class.new do 6 | include Mutant::Anima.new(:argument), Mutant::Procto 7 | 8 | def call 9 | argument 10 | end 11 | end 12 | end 13 | 14 | it 'creates a .call that proxies to #call' do 15 | argument = +'foo' 16 | 17 | expect(klass.call(argument:)).to be(argument) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/test/result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::Test::Result do 4 | setup_shared_context 5 | 6 | let(:reportable) do 7 | Mutant::Result::Test.new( 8 | job_index: 0, 9 | output: '', 10 | passed: false, 11 | runtime: 0.1 12 | ) 13 | end 14 | 15 | it_reports <<~'STR' 16 | 17 | STR 18 | end 19 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Namespace for class mutations 7 | class Class < self 8 | handle :class 9 | 10 | children :klass, :parent, :body 11 | 12 | private 13 | 14 | def dispatch 15 | emit_body_mutations if body 16 | end 17 | end # Class 18 | end # Node 19 | end # Mutator 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/selector/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Selector 5 | # Selector that never returns tests 6 | class Null < self 7 | include Equalizer.new 8 | 9 | # Tests for subject 10 | # 11 | # @param [Subject] subject 12 | # 13 | # @return [Enumerable] 14 | def call(_subject) 15 | EMPTY_ARRAY 16 | end 17 | end # Null 18 | end # Selector 19 | end # Mutant 20 | -------------------------------------------------------------------------------- /ruby/test_app/spec/unit/test_app/literal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe TestApp::Literal do 4 | 5 | describe '#command' do 6 | subject { object.command(double) } 7 | 8 | let(:object) { described_class.new } 9 | 10 | it { should be(object) } 11 | end 12 | 13 | describe '#string' do 14 | subject { object.string } 15 | 16 | let(:object) { described_class.new } 17 | 18 | it { should eql('string') } 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ruby/test_app/test/unit/test_app/literal_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'mutant/minitest/coverage' 3 | 4 | class LiteralTest < Minitest::Test 5 | cover 'TestApp::Literal*' 6 | cover 'TestApp::Literal#string' 7 | 8 | def test_command 9 | object = ::TestApp::Literal.new 10 | 11 | assert_equal(object, object.command('x')) 12 | end 13 | 14 | def test_string 15 | assert_equal('string', ::TestApp::Literal.new.string) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/meta/resbody.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Node meta information mixin 6 | module Meta 7 | 8 | # Metadata for resbody nodes 9 | class Resbody 10 | include NamedChildren, Anima.new(:node) 11 | 12 | children :captures, :assignment, :body 13 | 14 | public :captures, :assignment, :body 15 | end # Resbody 16 | 17 | end # Meta 18 | end # AST 19 | end # Mutant 20 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # A null matcher, that does not match any subjects 6 | class Null < self 7 | include Concord.new 8 | 9 | # Enumerate subjects 10 | # 11 | # @param [Env] env 12 | # 13 | # @return [Enumerable] 14 | def call(_env) 15 | EMPTY_ARRAY 16 | end 17 | 18 | end # Null 19 | end # Matcher 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/minitest_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'minitest integration', mutant: false do 4 | let(:base_cmd) do 5 | %w[ 6 | bundle exec mutant run 7 | --include test 8 | --include lib 9 | --require test_app 10 | --integration minitest 11 | --usage opensource 12 | ] 13 | end 14 | 15 | let(:gemfile) { 'Gemfile.minitest' } 16 | 17 | it_behaves_like 'framework integration' 18 | end 19 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/numblock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Numblock < self 7 | 8 | handle(:numblock) 9 | 10 | children :receiver, :count, :block 11 | 12 | private 13 | 14 | def dispatch 15 | emit_nil 16 | emit_receiver_mutations(&method(:n_send?)) 17 | end 18 | end # Numblock 19 | end # Node 20 | end # Mutator 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/resbody.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for resbody nodes 7 | class Resbody < self 8 | 9 | handle(:resbody) 10 | 11 | children :captures, :assignment, :body 12 | 13 | private 14 | 15 | def dispatch 16 | emit_body_mutations if body 17 | end 18 | end # Resbody 19 | end # Node 20 | end # Mutator 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal 7 | # Mutator for string literals 8 | class String < self 9 | 10 | handle(:str) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons 16 | end 17 | 18 | end # String 19 | end # Literal 20 | end # Node 21 | end # Mutator 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/masgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutation emitter to handle multiple assignment nodes 7 | class Masgn < self 8 | 9 | handle(:masgn) 10 | 11 | children :left, :right 12 | 13 | private 14 | 15 | def dispatch 16 | emit_right_mutations 17 | end 18 | 19 | end # Masgn 20 | end # Node 21 | end # Mutator 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/sclass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Namespace for singleton class mutations (class << some_obj) 7 | class Sclass < self 8 | handle :sclass 9 | 10 | children :expr, :body 11 | 12 | private 13 | 14 | def dispatch 15 | emit_body_mutations if body 16 | end 17 | end # Sclass 18 | end # Node 19 | end # Mutator 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/send/conditional.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Send 7 | class Conditional < self 8 | 9 | handle(:csend) 10 | 11 | private 12 | 13 | def dispatch 14 | super 15 | emit(s(:send, *children)) 16 | end 17 | 18 | end # Conditional 19 | end # Send 20 | end # Node 21 | end # Mutator 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/yield.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Yield mutator 8 | class Yield < Generic 9 | 10 | handle(:yield) 11 | 12 | private 13 | 14 | def dispatch 15 | super 16 | emit_singletons 17 | children.each_index(&method(:delete_child)) 18 | end 19 | 20 | end # Yield 21 | end # Node 22 | end # Mutator 23 | end # Mutant 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/filter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher filter 6 | class Filter < self 7 | include Anima.new(:matcher, :predicate) 8 | 9 | # Enumerate matches 10 | # 11 | # @param [Env] env 12 | # 13 | # @return [Enumerable] 14 | def call(env) 15 | matcher.call(env).select(&predicate) 16 | end 17 | 18 | end # Filter 19 | end # Matcher 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/and_asgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # AndAsgn mutator 8 | class AndAsgn < self 9 | 10 | handle(:and_asgn) 11 | 12 | children :left, :right 13 | 14 | private 15 | 16 | def dispatch 17 | emit_left_mutations 18 | emit_right_mutations 19 | end 20 | 21 | end # AndAsgn 22 | end # Node 23 | end # Mutator 24 | end # Mutant 25 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/generic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Generic mutator 7 | class Generic < self 8 | 9 | private 10 | 11 | def dispatch 12 | children.each_with_index do |child, index| 13 | mutate_child(index) if child.instance_of?(::Parser::AST::Node) 14 | end 15 | end 16 | 17 | end # Generic 18 | end # Node 19 | end # Mutator 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/meta/super.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :super do 4 | source 'super' 5 | 6 | singleton_mutations 7 | end 8 | 9 | Mutant::Meta::Example.add :super do 10 | source 'super()' 11 | 12 | singleton_mutations 13 | end 14 | 15 | Mutant::Meta::Example.add :super do 16 | source 'super(foo, bar)' 17 | 18 | singleton_mutations 19 | mutation 'super(foo)' 20 | mutation 'super(bar)' 21 | mutation 'super(foo, nil)' 22 | mutation 'super(nil, bar)' 23 | end 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/break.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for loop control keywords 8 | class Break < Generic 9 | 10 | handle(:break) 11 | 12 | private 13 | 14 | def dispatch 15 | super 16 | emit_singletons 17 | children.each_index(&method(:delete_child)) 18 | end 19 | 20 | end # Break 21 | end # Node 22 | end # Mutator 23 | end # Mutant 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/procto.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module Procto 5 | # Define the .call method on +host+ 6 | # 7 | # @param [Object] host 8 | # the hosting object 9 | # 10 | # @return [undefined] 11 | # 12 | # @api private 13 | def self.included(host) 14 | host.extend(ClassMethods) 15 | end 16 | 17 | module ClassMethods 18 | def call(*) 19 | new(*).call 20 | end 21 | end 22 | end # Procto 23 | end # Unparser 24 | -------------------------------------------------------------------------------- /ruby/meta/lambda.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :block, :lambda do 4 | source '->() {}' 5 | 6 | singleton_mutations 7 | 8 | mutation '->() { raise }' 9 | end 10 | 11 | Mutant::Meta::Example.add :block, :lambda do 12 | source '->() { foo.bar }' 13 | 14 | singleton_mutations 15 | 16 | mutation '->() { }' 17 | mutation '->() { nil }' 18 | mutation '->() { raise }' 19 | mutation '->() { self.bar }' 20 | mutation '->() { foo }' 21 | mutation 'foo.bar' 22 | end 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/splat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for splat nodes 7 | class Splat < self 8 | 9 | handle :splat 10 | 11 | children :expression 12 | 13 | private 14 | 15 | def dispatch 16 | emit_singletons 17 | emit_expression_mutations 18 | emit(expression) 19 | end 20 | 21 | end # Splat 22 | end # Node 23 | end # Mutator 24 | end # Mutant 25 | -------------------------------------------------------------------------------- /ruby/lib/mutant/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | 5 | module Mutant 6 | # Current mutant version 7 | # 8 | # See RUST.md for documentation on version loading behavior. 9 | VERSION = 10 | if ENV['MUTANT_RUST'] 11 | ENV.fetch('MUTANT_VERSION').freeze 12 | else 13 | Pathname 14 | .new(__dir__) 15 | .parent 16 | .parent 17 | .join('VERSION') 18 | .read 19 | .chomp 20 | .freeze 21 | end 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/expression/descendants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Expression 5 | class Descendants < self 6 | include Anima.new(:const_name) 7 | 8 | REGEXP = /\Adescendants:(?.+)\z/ 9 | 10 | def syntax 11 | "descendants:#{const_name}" 12 | end 13 | 14 | # rubocop:disable Lint/UnusedMethodArgument 15 | def matcher(env:) 16 | Matcher::Descendants.new(const_name:) 17 | end 18 | end # Descendants 19 | end # Expression 20 | end # Mutant 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/next.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for loop control keywords 8 | class Next < Generic 9 | 10 | handle(:next) 11 | 12 | private 13 | 14 | def dispatch 15 | super 16 | emit_singletons 17 | children.each_index(&method(:delete_child)) 18 | emit(s(:break, *children)) 19 | end 20 | 21 | end # Next 22 | end # Node 23 | end # Mutator 24 | end # Mutant 25 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/match_current_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Emitter for perl style match current line node 7 | class MatchCurrentLine < self 8 | 9 | handle :match_current_line 10 | 11 | children :regexp 12 | 13 | private 14 | 15 | def dispatch 16 | emit_singletons 17 | emit_regexp_mutations 18 | end 19 | 20 | end # MatchCurrentLine 21 | end # Node 22 | end # Mutator 23 | end # Mutant 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/return.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for return statements 7 | class Return < self 8 | 9 | handle(:return) 10 | 11 | children :value 12 | 13 | private 14 | 15 | def dispatch 16 | emit_singletons 17 | return unless value 18 | emit(value) 19 | emit_value_mutations 20 | end 21 | 22 | end # Return 23 | end # Node 24 | end # Mutator 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /docs/commercial-support.md: -------------------------------------------------------------------------------- 1 | # Commercial Support 2 | 3 | Mutant offers only community support. 4 | 5 | Commercial license customers are entited to priority support via email. 6 | 7 | ## Priority Support 8 | 9 | Covers 1 incident per quarter, with a max response time of 7 days. 10 | Scope is limited to mutant not the application or infrastructure. 11 | 12 | For support email [Markus Schirp](mailto:mbj@schirp-dso.com?subject=Mutant%20Support). 13 | Please email using the same domain as the original license email or explain 14 | your connection to the license. 15 | -------------------------------------------------------------------------------- /mutant/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mutant" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors = ["Markus Schirp "] 6 | license = "Nonstandard" 7 | description = "Mutation Testing" 8 | homepage = "https://github.com/mbj/mutant" 9 | repository = "https://github.com/mbj/mutant" 10 | readme = "../README.md" 11 | 12 | [[bin]] 13 | name = "mutant" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | clap = { version = "4.5", features = ["derive"] } 18 | log = "0.4" 19 | env_logger = "0.11" 20 | 21 | [dev-dependencies] 22 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/chain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher chaining results of other matchers together 6 | class Chain < self 7 | include Anima.new(:matchers) 8 | 9 | # Call matcher 10 | # 11 | # @param [Env] env 12 | # 13 | # @return [Enumerable] 14 | def call(env) 15 | matchers.flat_map do |matcher| 16 | matcher.call(env) 17 | end.uniq 18 | end 19 | 20 | end # Chain 21 | end # Matcher 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/nthref.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for nth-ref nodes 7 | class NthRef < self 8 | 9 | handle :nth_ref 10 | 11 | children :number 12 | 13 | private 14 | 15 | def dispatch 16 | unless number.equal?(1) 17 | emit_number(number - 1) 18 | end 19 | emit_number(number + 1) 20 | end 21 | 22 | end # NthRef 23 | end # Node 24 | end # Mutator 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/coverage_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Reporter for mutation coverage results 8 | class CoverageResult < self 9 | # Run report printer 10 | # 11 | # @return [undefined] 12 | def run 13 | visit(MutationResult, object.mutation_result) 14 | end 15 | end # Printer 16 | end # Coverage 17 | end # CLI 18 | end # Reporter 19 | end # Mutant 20 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/super.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for super with parentheses 8 | class Super < self 9 | 10 | handle(:super) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons 16 | children.each_index do |index| 17 | mutate_child(index) 18 | delete_child(index) 19 | end 20 | end 21 | 22 | end # Super 23 | end # Node 24 | end # Mutator 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/node_predicates.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Module for node predicates 6 | module NodePredicates 7 | 8 | Types::ALL.each do |type| 9 | fail "method: #{type} is already defined" if instance_methods(true).include?(type) 10 | 11 | name = "n_#{type.to_s.chomp('?')}?" 12 | 13 | define_method(name) do |node| 14 | node.type.equal?(type) 15 | end 16 | private name 17 | end 18 | 19 | end # NodePredicates 20 | end # AST 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/lib/mutant/segment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Segment 5 | include Adamantium, Anima.new( 6 | :id, 7 | :name, 8 | :parent_id, 9 | :timestamp_end, 10 | :timestamp_start 11 | ) 12 | 13 | def elapsed 14 | timestamp_end - timestamp_start 15 | end 16 | 17 | def offset_start(recording_start) 18 | timestamp_start - recording_start 19 | end 20 | 21 | def offset_end(recording_start) 22 | timestamp_end - recording_start 23 | end 24 | end # Segment 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/dynamic_literal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for dynamic literals 7 | class DynamicLiteral < self 8 | 9 | handle(:dstr, :dsym, :xstr) 10 | 11 | private 12 | 13 | def dispatch 14 | emit_singletons 15 | 16 | children.each_index do |index| 17 | mutate_child(index, &method(:n_begin?)) 18 | end 19 | end 20 | 21 | end # DynamicLiteral 22 | end # Node 23 | end # Mutator 24 | end # Mutant 25 | -------------------------------------------------------------------------------- /ruby/meta/casgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :casgn do 4 | source 'A = true' 5 | 6 | mutation 'A__MUTANT__ = true' 7 | mutation 'A = false' 8 | mutation 'remove_const :A' 9 | end 10 | 11 | Mutant::Meta::Example.add :casgn do 12 | source 'self::A = true' 13 | 14 | mutation 'self::A__MUTANT__ = true' 15 | mutation 'self::A = false' 16 | mutation 'self.remove_const :A' 17 | end 18 | 19 | Mutant::Meta::Example.add :casgn do 20 | source 'A &&= true' 21 | 22 | mutation 'A__MUTANT__ &&= true' 23 | mutation 'A &&= false' 24 | end 25 | -------------------------------------------------------------------------------- /ruby/test_app/lib/test_app/literal.rb: -------------------------------------------------------------------------------- 1 | module TestApp 2 | # Class for integration testing literal mutations 3 | class Literal 4 | def boolean 5 | true 6 | end 7 | 8 | def command(_foo) 9 | self 10 | end 11 | 12 | def string 13 | 'string' 14 | end 15 | 16 | def uncovered_string 17 | 'string' 18 | end 19 | 20 | def self.string 21 | 'string' 22 | end 23 | 24 | def symbol 25 | :symbol 26 | end 27 | 28 | def float 29 | 2.4 30 | end 31 | end 32 | 33 | class Empty 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/regopt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Regular expression options mutation 8 | class Regopt < self 9 | 10 | MUTATED_FLAGS = %i[i].freeze 11 | 12 | handle(:regopt) 13 | 14 | private 15 | 16 | def dispatch 17 | emit_type(*mutated_flags) 18 | end 19 | 20 | def mutated_flags 21 | (children - MUTATED_FLAGS) 22 | end 23 | 24 | end # Regopt 25 | end # Node 26 | end # Mutator 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/lib/mutant/cli/command/environment/irb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module CLI 5 | class Command 6 | class Environment 7 | class IRB < self 8 | NAME = 'irb' 9 | SHORT_DESCRIPTION = 'Run irb with mutant environment loaded' 10 | SUBCOMMANDS = EMPTY_ARRAY 11 | 12 | private 13 | 14 | def action 15 | bootstrap.fmap { TOPLEVEL_BINDING.irb } 16 | end 17 | end # IRB 18 | end # Environment 19 | end # Command 20 | end # CLI 21 | end # Mutant 22 | -------------------------------------------------------------------------------- /ruby/meta/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :array do 4 | source '[true]' 5 | 6 | singleton_mutations 7 | mutation 'true' 8 | mutation '[false]' 9 | mutation '[]' 10 | end 11 | 12 | Mutant::Meta::Example.add :array do 13 | source '[true, false]' 14 | 15 | singleton_mutations 16 | 17 | # Mutation of each element in array 18 | mutation '[false, false]' 19 | mutation '[true, true]' 20 | 21 | # Remove each element of array once 22 | mutation '[true]' 23 | mutation '[false]' 24 | 25 | # Empty array 26 | mutation '[]' 27 | end 28 | -------------------------------------------------------------------------------- /ruby/meta/csend.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :csend do 4 | source 'a&.b' 5 | 6 | singleton_mutations 7 | mutation 'a.b' 8 | mutation 'self&.b' 9 | mutation 'a' 10 | end 11 | 12 | Mutant::Meta::Example.add :csend do 13 | source 'a&.public_send(:b)' 14 | 15 | singleton_mutations 16 | mutation ':b' 17 | mutation 'a.public_send(:b)' 18 | mutation 'a' 19 | mutation 'a&.public_send' 20 | mutation 'a&.public_send(:b__mutant__)' 21 | mutation 'a&.public_send(nil)' 22 | mutation 'self&.public_send(:b)' 23 | mutation 'a&.b' 24 | end 25 | -------------------------------------------------------------------------------- /ruby/meta/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :hash do 4 | source '{true => true, false => false}' 5 | 6 | singleton_mutations 7 | 8 | # Mutation of each key and value in hash 9 | mutation '{ false => true , false => false }' 10 | mutation '{ true => false , false => false }' 11 | mutation '{ true => true , true => false }' 12 | mutation '{ true => true , false => true }' 13 | 14 | # Remove each key once 15 | mutation '{ true => true }' 16 | mutation '{ false => false }' 17 | 18 | # Empty hash 19 | mutation '{}' 20 | end 21 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/symbol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Mutator for symbol literals 8 | class Symbol < self 9 | 10 | handle(:sym) 11 | 12 | children :value 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | 19 | Util::Symbol.call(input: value, parent: nil).each(&method(:emit_type)) 20 | end 21 | end # Symbol 22 | end # Literal 23 | end # Node 24 | end # Mutator 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/cli/command/root.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module CLI 5 | class Command 6 | class Environment < self 7 | SUBCOMMANDS = [Environment::Subject, Environment::Show, Environment::IRB, Environment::Test].freeze 8 | end # Environment 9 | 10 | class Root < self 11 | NAME = 'mutant' 12 | SHORT_DESCRIPTION = 'mutation testing engine main command' 13 | SUBCOMMANDS = [Environment::Run, Environment::Test::Run::Root, Environment, Util].freeze 14 | end # Root 15 | end # Command 16 | end # CLI 17 | end # Mutant 18 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/corpus_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Mutant on ruby corpus', mutant: false do 4 | MutantSpec::Corpus::Project::ALL.select(&:mutation_generation).each do |project| 5 | specify "#{project.name} does not fail on mutation generation" do 6 | project.verify_mutation_generation 7 | end 8 | end 9 | 10 | MutantSpec::Corpus::Project::ALL.select(&:mutation_coverage).each do |project| 11 | specify "#{project.name} (#{project.integration_name}) does have expected mutation coverage" do 12 | project.verify_mutation_coverage 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/env_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Full env result reporter 8 | class EnvResult < self 9 | delegate(:failed_subject_results) 10 | 11 | # Run printer 12 | # 13 | # @return [undefined] 14 | def run 15 | visit_collection(SubjectResult, failed_subject_results) 16 | visit(EnvProgress, object) 17 | end 18 | end # EnvResult 19 | end # Printer 20 | end # CLI 21 | end # Reporter 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/pattern/source/location_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Pattern::Source::Location do 4 | let(:instance) do 5 | described_class.new( 6 | line_index: 1, 7 | line_start: 1, 8 | range: 3..4, 9 | source: 10 | ) 11 | end 12 | 13 | let(:source) do 14 | Mutant::AST::Pattern::Source.new(string: "\n1234") 15 | end 16 | 17 | describe '#display' do 18 | def apply 19 | instance.display 20 | end 21 | 22 | it 'returns expected value' do 23 | expect(apply).to eql("1234\n ^^") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/boolean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Abstract mutator for boolean literals 8 | class Boolean < self 9 | 10 | private 11 | 12 | MAP = { 13 | true: :false, 14 | false: :true 15 | }.freeze 16 | 17 | handle(*MAP.keys) 18 | 19 | def dispatch 20 | emit(s(MAP.fetch(node.type))) 21 | end 22 | 23 | end # Boolean 24 | end # Literal 25 | end # Node 26 | end # Mutator 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/meta/while.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :while do 4 | source 'while true; foo; bar; end' 5 | 6 | singleton_mutations 7 | mutation 'while true; bar; end' 8 | mutation 'while true; foo; end' 9 | mutation 'while true; end' 10 | mutation 'while false; foo; bar; end' 11 | mutation 'while true; foo; nil; end' 12 | mutation 'while true; nil; bar; end' 13 | mutation 'while true; raise; end' 14 | end 15 | 16 | Mutant::Meta::Example.add :while do 17 | source 'while true; end' 18 | 19 | singleton_mutations 20 | mutation 'while true; raise; end' 21 | mutation 'while false; end' 22 | end 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/conditional_loop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for while expressions 8 | class ConditionalLoop < self 9 | 10 | handle(:until, :while) 11 | 12 | children :condition, :body 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | emit_condition_mutations 19 | emit_body_mutations if body 20 | emit_body(nil) 21 | emit_body(N_RAISE) 22 | end 23 | 24 | end # ConditionalLoop 25 | end # Node 26 | end # Mutator 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/util/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Util 6 | 7 | # Mutators that mutates an array of inputs 8 | class Array < self 9 | 10 | # Element presence mutator 11 | class Presence < Util 12 | 13 | private 14 | 15 | def dispatch 16 | input.each_index do |index| 17 | dup = dup_input 18 | dup.delete_at(index) 19 | emit(dup) 20 | end 21 | end 22 | 23 | end # Presence 24 | end # Array 25 | end # Util 26 | end # Mutator 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/meta/const.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Node meta information mixin 6 | module Meta 7 | 8 | # Metadata for const nodes 9 | class Const 10 | include NamedChildren, Anima.new(:node), NodePredicates 11 | 12 | children :base, :name 13 | 14 | public :base, :name 15 | 16 | # Test if AST node is possibly a top level constant 17 | # 18 | # @return [Boolean] 19 | def possible_top_level? 20 | base.nil? || n_cbase?(base) 21 | end 22 | 23 | end # Const 24 | end # Meta 25 | end # AST 26 | end # Mutant 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/integer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Mutator for integer literals 8 | class Integer < self 9 | 10 | handle(:int) 11 | 12 | children :value 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | emit_values 19 | end 20 | 21 | def values 22 | [0, 1, value + 1, value - 1] 23 | end 24 | 25 | end # Integer 26 | end # Literal 27 | end # Node 28 | end # Mutator 29 | end # Mutant 30 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/success_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Success do 4 | subject { described_class.new(block:) } 5 | 6 | let(:block) { ->(value) { value * 2 } } 7 | 8 | describe '#call' do 9 | def apply 10 | subject.call(input) 11 | end 12 | 13 | let(:input) { 3 } 14 | 15 | it 'returns success' do 16 | expect(apply).to eql(right(6)) 17 | end 18 | end 19 | 20 | describe '#slug' do 21 | def apply 22 | subject.slug 23 | end 24 | 25 | it 'returns name' do 26 | expect(apply).to eql(described_class.name) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/or_asgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # OrAsgn mutator 8 | class OrAsgn < self 9 | 10 | handle(:or_asgn) 11 | 12 | children :left, :right 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | emit_right_mutations 19 | return if n_ivasgn?(left) 20 | emit_left_mutations do |node| 21 | AST::Types::ASSIGNABLE_VARIABLES.include?(node.type) 22 | end 23 | end 24 | 25 | end # OrAsgn 26 | end # Node 27 | end # Mutator 28 | end # Mutant 29 | -------------------------------------------------------------------------------- /ruby/meta/indexasgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :indexasgn do 4 | source 'foo[bar] = baz' 5 | 6 | singleton_mutations 7 | mutation 'self[bar] = baz' 8 | mutation 'foo' 9 | mutation 'foo[bar]' 10 | mutation 'foo.at(bar)' 11 | mutation 'foo.fetch(bar)' 12 | mutation 'foo.key?(bar)' 13 | mutation 'foo[bar] = nil' 14 | mutation 'foo[nil] = baz' 15 | mutation 'foo[] = baz' 16 | mutation 'baz' 17 | end 18 | 19 | Mutant::Meta::Example.add :indexasgn, :op_asgn do 20 | source 'self[foo] += bar' 21 | 22 | mutation 'self[] += bar' 23 | mutation 'self[nil] += bar' 24 | mutation 'self[foo] += nil' 25 | end 26 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/structure_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Structure do 4 | describe '.for' do 5 | def apply 6 | described_class.for(type) 7 | end 8 | 9 | let(:type) { :str } 10 | 11 | it 'returns expected structure' do 12 | expect(apply).to eql( 13 | described_class::Node.new( 14 | type: :str, 15 | fixed: [ 16 | described_class::Node::Fixed::Attribute.new( 17 | index: 0, 18 | name: :value 19 | ) 20 | ], 21 | variable: nil 22 | ) 23 | ) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/isolation/exception.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Module providing isolation 5 | class Isolation 6 | # Generic serializable exception data. 7 | # 8 | # This is required as our honored guests the Rails* ecosystem 9 | # makes Marshal.dump on exceptions impossible. 10 | # 11 | # @see https://twitter.com/_m_b_j_/status/1356433184850907137 12 | # 13 | # for the full story and eventual reactions. 14 | class Exception 15 | include Anima.new( 16 | :backtrace, 17 | :message, 18 | :original_class 19 | ) 20 | end # Exception 21 | end # Isolation 22 | end # Mutant 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/sequence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class Sequence < self 6 | include Anima.new(:reporters) 7 | 8 | %i[warn progress report start].each do |name| 9 | define_method(name) do |value| 10 | reporters.each do |reporter| 11 | reporter.public_send(name, value) 12 | end 13 | 14 | self 15 | end 16 | end 17 | 18 | # Minimum reporter delay 19 | # 20 | # @return [Float] 21 | def delay 22 | reporters.map(&:delay).min 23 | end 24 | 25 | end # Sequence 26 | end # Reporter 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/meta/op_assgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :op_asgn, :send do 4 | source '@a.b += 1' 5 | 6 | mutation '@a += 1' 7 | mutation '@a.b += 0' 8 | mutation '@a.b += 2' 9 | mutation '@a.b += nil' 10 | mutation 'a.b += 1' 11 | mutation 'self.b += 1' 12 | end 13 | 14 | Mutant::Meta::Example.add :op_asgn, :send do 15 | source 'a.b += 1' 16 | 17 | mutation 'a.b += 0' 18 | mutation 'a.b += 2' 19 | mutation 'a.b += nil' 20 | mutation 'self.b += 1' 21 | end 22 | 23 | Mutant::Meta::Example.add :op_asgn, :send do 24 | source 'b += 1' 25 | 26 | mutation 'b += 0' 27 | mutation 'b += 2' 28 | mutation 'b += nil' 29 | end 30 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Result do 4 | let(:object) do 5 | Class.new do 6 | include Mutant::Result, Unparser::Anima.new(:runtime, :killtime) 7 | 8 | def collection 9 | [[1]] 10 | end 11 | 12 | sum :length, :collection 13 | end.new(runtime: 3.0, killtime: 1.0) 14 | end 15 | 16 | describe '.included' do 17 | it 'includes mixin to freeze instances' do 18 | expect(object.frozen?).to be(true) 19 | end 20 | 21 | it 'it makes DSL methods from Mutant::Result available' do 22 | expect(object.length).to be(1) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/const.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutation emitter to handle const nodes 8 | class Const < self 9 | 10 | handle(:const) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons unless parent_node && n_const?(parent_node) 16 | emit_type(nil, *children.drop(1)) 17 | children.each_with_index do |child, index| 18 | mutate_child(index) if child.instance_of?(::Parser::AST::Node) 19 | end 20 | end 21 | 22 | end # Const 23 | end # Node 24 | end # Mutator 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_capture_group.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_capture_group do 4 | source '/()/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | end 9 | 10 | Mutant::Meta::Example.add :regexp_capture_group do 11 | source '/(foo|bar)/' 12 | 13 | singleton_mutations 14 | regexp_mutations 15 | 16 | mutation '/(?:foo|bar)/' 17 | mutation '/(foo)/' 18 | mutation '/(bar)/' 19 | end 20 | 21 | Mutant::Meta::Example.add :regexp_capture_group do 22 | source '/(\w\d)/' 23 | 24 | singleton_mutations 25 | regexp_mutations 26 | 27 | mutation '/(?:\w\d)/' 28 | mutation '/(\W\d)/' 29 | mutation '/(\w\D)/' 30 | end 31 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_named_group.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_named_group do 4 | source '/(?)/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | end 9 | 10 | Mutant::Meta::Example.add :regexp_named_group do 11 | source '/(?\w)/' 12 | 13 | singleton_mutations 14 | regexp_mutations 15 | 16 | mutation '/(?:\w)/' 17 | mutation '/(?\W)/' 18 | mutation '/(?<_foo>\w)/' 19 | end 20 | 21 | Mutant::Meta::Example.add :regexp_named_group do 22 | source '/(?<_foo>\w\d)/' 23 | 24 | singleton_mutations 25 | regexp_mutations 26 | 27 | mutation '/(?<_foo>\W\d)/' 28 | mutation '/(?<_foo>\w\D)/' 29 | end 30 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/sexp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Mixin for node sexp syntax 6 | module Sexp 7 | 8 | private 9 | 10 | # Build node 11 | # 12 | # @param [Symbol] type 13 | # 14 | # @return [Parser::AST::Node] 15 | def s(type, *children) 16 | ::Parser::AST::Node.new(type, children) 17 | end 18 | 19 | # Build a negated boolean node 20 | # 21 | # @param [Parser::AST::Node] node 22 | # 23 | # @return [Parser::AST::Node] 24 | def n_not(node) 25 | s(:send, node, :!) 26 | end 27 | 28 | end # Sexp 29 | end # AST 30 | end # Mutant 31 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/meta/optarg_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Meta::Optarg do 4 | subject(:object) { described_class.new(node:) } 5 | 6 | let(:node) { s(:optarg, name, value) } 7 | let(:name) { :foo } 8 | let(:value) { s(:sym, :bar) } 9 | 10 | its(:name) { should be(:foo) } 11 | its(:default_value) { should eql(s(:sym, :bar)) } 12 | 13 | describe '#used?' do 14 | subject { object.used? } 15 | 16 | it { should be true } 17 | 18 | context 'when name is prefixed with an underscore' do 19 | let(:name) { :_foo } 20 | it { should be false } 21 | end 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/meta/optarg.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Node meta information mixin 6 | module Meta 7 | 8 | # Metadata for optional argument nodes 9 | class Optarg 10 | include NamedChildren, Anima.new(:node) 11 | 12 | UNDERSCORE = '_' 13 | 14 | children :name, :default_value 15 | 16 | public :name, :default_value 17 | 18 | # Test if optarg definition intends to be used 19 | # 20 | # @return [Boolean] 21 | def used? 22 | !name.start_with?(UNDERSCORE) 23 | end 24 | end # Optarg 25 | 26 | end # Meta 27 | end # AST 28 | end # Mutant 29 | -------------------------------------------------------------------------------- /ruby/lib/mutant/cli/command/environment/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module CLI 5 | class Command 6 | class Environment 7 | class Show < self 8 | NAME = 'show' 9 | SHORT_DESCRIPTION = 'Display environment without coverage analysis' 10 | SUBCOMMANDS = EMPTY_ARRAY 11 | 12 | private 13 | 14 | def action 15 | bootstrap.fmap(&method(:report_env)) 16 | end 17 | 18 | def report_env(env) 19 | env.config.reporter.start(env) 20 | end 21 | end # Show 22 | end # Environment 23 | end # Command 24 | end # CLI 25 | end # Mutant 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/subject/method/singleton.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Subject 5 | class Method 6 | # Singleton method subjects 7 | class Singleton < self 8 | 9 | NAME_INDEX = 1 10 | SYMBOL = '.' 11 | 12 | # Prepare subject for mutation insertion 13 | # 14 | # @return [self] 15 | def prepare 16 | scope.raw.singleton_class.undef_method(name) 17 | self 18 | end 19 | 20 | def post_insert 21 | scope.raw.singleton_class.__send__(visibility, name) 22 | self 23 | end 24 | 25 | end # Singleton 26 | end # Method 27 | end # Subject 28 | end # Mutant 29 | -------------------------------------------------------------------------------- /ruby/meta/float.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :float do 4 | source '10.0' 5 | 6 | singleton_mutations 7 | 8 | # edge cases 9 | mutation '0.0' 10 | mutation '1.0' 11 | mutation '0.0 / 0.0' 12 | mutation '1.0 / 0.0' 13 | mutation '-1.0 / 0.0' 14 | end 15 | 16 | Mutant::Meta::Example.add :float do 17 | source '0.0' 18 | 19 | singleton_mutations 20 | mutation '1.0' 21 | mutation '0.0 / 0.0' 22 | mutation '1.0 / 0.0' 23 | mutation '-1.0 / 0.0' 24 | end 25 | 26 | Mutant::Meta::Example.add :float do 27 | source '-0.0' 28 | 29 | singleton_mutations 30 | mutation '1.0' 31 | mutation '0.0 / 0.0' 32 | mutation '1.0 / 0.0' 33 | mutation '-1.0 / 0.0' 34 | end 35 | -------------------------------------------------------------------------------- /ruby/lib/mutant/integration/null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Integration 5 | # Null integration that has no tests 6 | class Null < self 7 | # Available tests for integration 8 | # 9 | # @return [Enumerable] 10 | def all_tests 11 | EMPTY_ARRAY 12 | end 13 | 14 | # Run a collection of tests 15 | # 16 | # @param [Enumerable] tests 17 | # 18 | # @return [Result::Test] 19 | def call(_tests) 20 | Result::Test.new( 21 | job_index: nil, 22 | output: '', 23 | passed: true, 24 | runtime: 0.0 25 | ) 26 | end 27 | 28 | end # Null 29 | end # Integration 30 | end # Mutant 31 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/descendants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher for all descendants by constant name 6 | class Descendants < self 7 | include Anima.new(:const_name) 8 | 9 | def call(env) 10 | const = env.world.try_const_get(const_name) or return EMPTY_ARRAY 11 | 12 | Chain.new( 13 | matchers: matched_scopes(env, const).map { |scope| Scope.new(scope:) } 14 | ).call(env) 15 | end 16 | 17 | private 18 | 19 | def matched_scopes(env, const) 20 | env.matchable_scopes.select do |scope| 21 | scope.raw.equal?(const) || const > scope.raw 22 | end 23 | end 24 | end # Descendant 25 | end # Matcher 26 | end # Mutant 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/subject_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Subject result printer 8 | class SubjectResult < self 9 | 10 | delegate :subject, :uncovered_results, :tests 11 | 12 | # Run report printer 13 | # 14 | # @return [undefined] 15 | def run 16 | status(subject.identification) 17 | tests.each do |test| 18 | puts("- #{test.identification}") 19 | end 20 | visit_collection(CoverageResult, uncovered_results) 21 | end 22 | 23 | end # SubjectResult 24 | end # Printer 25 | end # CLI 26 | end # Reporter 27 | end # Mutant 28 | -------------------------------------------------------------------------------- /ruby/lib/mutant/require_highjack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Require highjack 5 | module RequireHighjack 6 | 7 | # Install require callback 8 | # 9 | # @param [Module] target 10 | # @param [#call] callback 11 | # 12 | # @return [#call] 13 | # the original implementation on singleton 14 | def self.call(target, callback) 15 | target.public_method(:require).tap do 16 | target.module_eval do 17 | undef_method(:require) 18 | define_method(:require, &callback) 19 | class << self 20 | undef_method(:require) 21 | end 22 | define_singleton_method(:require, &callback) 23 | end 24 | end 25 | end 26 | 27 | end # RequireHighjack 28 | end # Mutant 29 | -------------------------------------------------------------------------------- /ruby/lib/mutant/isolation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Isolation mechanism 5 | class Isolation 6 | include AbstractType 7 | 8 | # Isolated computation result 9 | class Result 10 | include Anima.new( 11 | :exception, 12 | :log, 13 | :process_status, 14 | :timeout, 15 | :value 16 | ) 17 | 18 | # Test for successful result 19 | # 20 | # @return [Boolean] 21 | def valid_value? 22 | timeout.nil? && exception.nil? && (process_status.nil? || process_status.success?) 23 | end 24 | end # Result 25 | 26 | # Call block in isolation 27 | # 28 | # @return [Result] 29 | # the blocks result 30 | abstract_method :call 31 | end # Isolation 32 | end # Mutant 33 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/nodes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | # Singleton nodes 6 | module Nodes 7 | extend Sexp 8 | 9 | N_NAN = s(:send, s(:float, 0.0), :/, s(:float, 0.0)) 10 | N_INFINITY = s(:send, s(:float, 1.0), :/, s(:float, 0.0)) 11 | N_NEGATIVE_INFINITY = s(:send, s(:float, -1.0), :/, s(:float, 0.0)) 12 | N_RAISE = s(:send, nil, :raise) 13 | N_TRUE = s(:true) 14 | N_FALSE = s(:false) 15 | N_NIL = s(:nil) 16 | N_EMPTY = s(:empty) 17 | N_SELF = s(:self) 18 | N_ZSUPER = s(:zsuper) 19 | N_EMPTY_SUPER = s(:super) 20 | 21 | end # Nodes 22 | end # AST 23 | end # Mutant 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/subject/method/metaclass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Subject 5 | class Method 6 | # Singleton method defined using metaclass syntax 7 | # (class << self; def foo; end; end) 8 | class Metaclass < self 9 | include AST::Sexp 10 | 11 | NAME_INDEX = 0 12 | SYMBOL = '.' 13 | 14 | # Prepare subject for mutation insertion 15 | # 16 | # @return [self] 17 | def prepare 18 | scope.raw.singleton_class.undef_method(name) 19 | self 20 | end 21 | 22 | private 23 | 24 | def wrap_node(mutant) 25 | s(:sclass, AST::Nodes::N_SELF, mutant) 26 | end 27 | end # Metaclass 28 | end # Method 29 | end # Subject 30 | end # Mutant 31 | -------------------------------------------------------------------------------- /ruby/lib/mutant/subject/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Subject 5 | class Config 6 | include Adamantium, Anima.new(:inline_disable, :mutation) 7 | 8 | DEFAULT = new(inline_disable: false, mutation: Mutation::Config::DEFAULT) 9 | 10 | DISABLE_REGEXP = /(\s|^)mutant:disable(?:\s|$)/ 11 | SYNTAX_REGEXP = /\A(?:#|=begin\n)/ 12 | 13 | def self.parse(comments:, mutation:) 14 | new( 15 | inline_disable: comments.any? { |comment| DISABLE_REGEXP.match?(comment_body(comment)) }, 16 | mutation: 17 | ) 18 | end 19 | 20 | def self.comment_body(comment) 21 | comment.text.sub(SYNTAX_REGEXP, '') 22 | end 23 | private_class_method :comment_body 24 | end # Config 25 | end # Subject 26 | end # Mutant 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/namespace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher for specific namespace 6 | class Namespace < self 7 | include Anima.new(:expression) 8 | 9 | # Enumerate subjects 10 | # 11 | # @param [Env] env 12 | # 13 | # @return [Enumerable] 14 | def call(env) 15 | Chain.new( 16 | matchers: matched_scopes(env).map { |scope| Scope.new(scope:) } 17 | ).call(env) 18 | end 19 | 20 | private 21 | 22 | def matched_scopes(env) 23 | env 24 | .matchable_scopes 25 | .select(&method(:match?)) 26 | end 27 | 28 | def match?(scope) 29 | expression.prefix?(scope.expression) 30 | end 31 | 32 | end # Namespace 33 | end # Matcher 34 | end # Mutant 35 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/block_pass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class BlockPass < self 7 | 8 | handle(:block_pass) 9 | 10 | children :argument 11 | 12 | private 13 | 14 | def dispatch 15 | return unless argument 16 | emit_argument_mutations 17 | emit_symbol_to_proc_mutations 18 | end 19 | 20 | def emit_symbol_to_proc_mutations 21 | return unless n_sym?(argument) 22 | 23 | config 24 | .operators 25 | .selector_replacements 26 | .fetch(*argument, EMPTY_ARRAY).each do |method| 27 | emit_argument(s(:sym, method)) 28 | end 29 | end 30 | end # Block 31 | end # Node 32 | end # Mutator 33 | end # Mutant 34 | -------------------------------------------------------------------------------- /ruby/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | usage: commercial 3 | includes: 4 | - lib 5 | integration: 6 | name: rspec 7 | requires: 8 | - mutant 9 | - mutant/integration/rspec 10 | - mutant/meta 11 | mutation: 12 | operators: full 13 | timeout: 3600.0 14 | coverage_criteria: 15 | timeout: false 16 | matcher: 17 | subjects: 18 | - Mutant* 19 | ignore: 20 | - Mutant::Isolation::Fork::Parent#call 21 | - Mutant::Zombifier#call 22 | # Mutation only alive for ruby < 3.1 23 | - Mutant::Mutator::Node::Arguments#anonymous_block_arg? 24 | - Mutant::Mutator::Node::Arguments#emit_argument_presence 25 | - Mutant::Mutator::Node::Arguments#removed_block_arg? 26 | - Mutant::Mutator::Node::BlockPass#dispatch 27 | # Mutation only alive for ruby < 3.2 28 | - Mutant::Mutator::Node::Arguments#forward_type? 29 | - Mutant::Mutator::Node::Send#emit_argument_propagation 30 | -------------------------------------------------------------------------------- /ruby/lib/mutant/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # An AST Parser 5 | class Parser 6 | include Adamantium, Equalizer.new 7 | 8 | # Initialize object 9 | # 10 | # @return [undefined] 11 | def initialize 12 | @cache = {} 13 | end 14 | 15 | # Parse path into AST 16 | # 17 | # @param [Pathname] path 18 | # 19 | # @return [AST::Node] 20 | def call(path) 21 | @cache[path.expand_path] ||= parse(path.read) 22 | end 23 | 24 | private 25 | 26 | def parse(source) 27 | ast = Unparser.parse_ast_either(source).from_right 28 | 29 | AST.new( 30 | comment_associations: ::Parser::Source::Comment.associate_by_identity(ast.node, ast.comments), 31 | node: ast.node 32 | ) 33 | end 34 | 35 | end # Parser 36 | end # Mutant 37 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/defined.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Namespace for `defined?` mutations 7 | class Defined < self 8 | 9 | handle(:defined?) 10 | 11 | children :expression 12 | 13 | private 14 | 15 | def dispatch 16 | emit(N_NIL) 17 | emit_instance_variable_mutation 18 | end 19 | 20 | def emit_instance_variable_mutation 21 | return unless n_ivar?(expression) 22 | 23 | instance_variable_name = Mutant::Util.one(expression.children) 24 | 25 | emit( 26 | s(:send, nil, :instance_variable_defined?, 27 | s(:sym, instance_variable_name)) 28 | ) 29 | end 30 | 31 | end # Defined 32 | end # Node 33 | end # Mutator 34 | end # Mutant 35 | -------------------------------------------------------------------------------- /ruby/lib/mutant/selector/expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Selector 5 | # Expression based test selector 6 | class Expression < self 7 | include Anima.new(:integration) 8 | 9 | # Tests for subject 10 | # 11 | # @param [Subject] subject 12 | # 13 | # @return [Enumerable] 14 | def call(subject) 15 | subject.match_expressions.each do |match_expression| 16 | subject_tests = integration.available_tests.select do |test| 17 | test.expressions.any? do |test_expression| 18 | match_expression.prefix?(test_expression) 19 | end 20 | end 21 | return subject_tests if subject_tests.any? 22 | end 23 | 24 | EMPTY_ARRAY 25 | end 26 | 27 | end # Expression 28 | end # Selector 29 | end # Mutant 30 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/segment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Segment do 4 | let(:recording_start) { 10 } 5 | 6 | subject do 7 | described_class.new( 8 | id: SecureRandom.uuid, 9 | name: :test_segment, 10 | parent_id: nil, 11 | timestamp_end: 13, 12 | timestamp_start: 11 13 | ) 14 | end 15 | 16 | describe '#elapsed' do 17 | it 'returns expected value' do 18 | expect(subject.elapsed).to eql(2) 19 | end 20 | end 21 | 22 | describe '#offset_end' do 23 | it 'returns expected value' do 24 | expect(subject.offset_end(recording_start)).to eql(3) 25 | end 26 | end 27 | 28 | describe '#offset_start' do 29 | it 'returns expected value' do 30 | expect(subject.offset_start(recording_start)).to eql(1) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/float.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Mutator for float literals 8 | class Float < self 9 | 10 | handle(:float) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons 16 | emit_values 17 | emit_special_cases 18 | end 19 | 20 | SPECIAL = [ 21 | N_NAN, 22 | N_NEGATIVE_INFINITY, 23 | N_INFINITY 24 | ].freeze 25 | 26 | def emit_special_cases 27 | SPECIAL.each(&method(:emit)) 28 | end 29 | 30 | def values 31 | [0.0, 1.0] 32 | end 33 | 34 | end # Float 35 | end # Literal 36 | end # Node 37 | end # Mutator 38 | end # Mutant 39 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/op_asgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # OpAsgn mutator 8 | class OpAsgn < self 9 | 10 | handle(:op_asgn) 11 | 12 | children :left, :operation, :right 13 | 14 | private 15 | 16 | def dispatch 17 | left_mutations 18 | 19 | emit_right_mutations 20 | end 21 | 22 | def left_mutations 23 | emit_left_mutations do |node| 24 | !n_self?(node) 25 | end 26 | emit_left_promotion if n_send?(left) 27 | end 28 | 29 | def emit_left_promotion 30 | receiver = left.children.first 31 | 32 | emit_left(s(:ivasgn, *receiver)) if n_ivar?(receiver) 33 | end 34 | 35 | end # OpAsgn 36 | end # Node 37 | end # Mutator 38 | end # Mutant 39 | -------------------------------------------------------------------------------- /ruby/mutant-rspec.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/mutant/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'mutant-rspec' 7 | gem.version = Mutant::VERSION.dup 8 | gem.authors = ['Markus Schirp'] 9 | gem.email = ['mbj@schirp-dso.com'] 10 | gem.description = 'Rspec integration for mutant' 11 | gem.summary = gem.description 12 | gem.homepage = 'https://github.com/mbj/mutant' 13 | gem.license = 'Nonstandard' 14 | 15 | gem.require_paths = %w[lib] 16 | gem.files = %w[lib/mutant/integration/rspec.rb] 17 | gem.extra_rdoc_files = %w[LICENSE] 18 | 19 | gem.metadata['rubygems_mfa_required'] = 'true' 20 | 21 | gem.required_ruby_version = '>= 3.2' 22 | 23 | gem.add_dependency('mutant', "= #{gem.version}") 24 | gem.add_dependency('rspec-core', '>= 3.8.0', '< 4.0.0') 25 | end 26 | -------------------------------------------------------------------------------- /ruby/lib/mutant/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Utility methods 5 | module Util 6 | # Return only element in array if it contains exactly one member 7 | # 8 | # @param array [Array] 9 | # 10 | # @return [Object] first entry 11 | def self.one(array) 12 | Unparser::Util.one(array) 13 | end 14 | 15 | # Return only element in array if it contains max one member 16 | # 17 | # @param array [Array] 18 | # 19 | # @return [Object] first entry 20 | # @return [nil] if empty 21 | # 22 | # rubocop:disable Lint/EmptyInPattern 23 | def self.max_one(array) 24 | case array 25 | in [] 26 | in [value] 27 | value 28 | else 29 | fail Unparser::Util::SizeError, "expected size to be max 1 but size was #{array.size}" 30 | end 31 | end 32 | end # Util 33 | end # Mutant 34 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/matcher/filter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Matcher::Filter, '#call' do 4 | subject { object.call(env) } 5 | 6 | let(:object) { described_class.new(matcher:, predicate:) } 7 | let(:matcher) { instance_double(Mutant::Matcher) } 8 | let(:subject_a) { instance_double(Mutant::Subject) } 9 | let(:subject_b) { instance_double(Mutant::Subject) } 10 | let(:env) { instance_double(Mutant::Env) } 11 | let(:predicate) { ->(node) { node.eql?(subject_a) } } 12 | 13 | before do 14 | expect(matcher).to receive(:call) 15 | .with(env) 16 | .and_return([subject_a, subject_b]) 17 | end 18 | 19 | it 'returns subjects after filtering' do 20 | should eql([subject_a]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Generator for mutations 5 | class Mutator 6 | include( 7 | Adamantium, 8 | AbstractType, 9 | Anima.new(:input, :parent), 10 | Procto 11 | ) 12 | 13 | # Return output 14 | # 15 | # @return [Set] 16 | attr_reader :output 17 | 18 | alias_method :call, :output 19 | 20 | private 21 | 22 | def initialize(_attributes) 23 | super 24 | 25 | @output = Set.new 26 | 27 | dispatch 28 | end 29 | 30 | def new?(object) 31 | !object.eql?(input) 32 | end 33 | 34 | abstract_method :dispatch 35 | private :dispatch 36 | 37 | def emit(object) 38 | return unless new?(object) 39 | 40 | output << object 41 | end 42 | 43 | def dup_input 44 | input.dup 45 | end 46 | 47 | end # Mutator 48 | end # Mutant 49 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/send/attribute_assignment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Send 7 | # Mutator for attribute assignments 8 | class AttributeAssignment < self 9 | 10 | ATTRIBUTE_RANGE = (..-2) 11 | 12 | private_constant(*constants(false)) 13 | 14 | private 15 | 16 | def dispatch 17 | normal_dispatch 18 | emit_attribute_read 19 | end 20 | 21 | def mutate_arguments 22 | remaining_children_indices.each do |index| 23 | mutate_child(index) 24 | end 25 | end 26 | 27 | def emit_attribute_read 28 | emit_type(receiver, selector[ATTRIBUTE_RANGE].to_sym) 29 | end 30 | 31 | end # AttributeAssignment 32 | end # Send 33 | end # Node 34 | end # Mutator 35 | end # Mutant 36 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/scope_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Scope do 4 | describe '#unqualified_name' do 5 | subject { object.unqualified_name } 6 | 7 | let(:object) do 8 | Mutant::Scope.new( 9 | expression: instance_double(Mutant::Expression), 10 | raw: raw_scope 11 | ) 12 | end 13 | 14 | context 'with top level constant name' do 15 | let(:raw_scope) { TestApp } 16 | 17 | it 'should return the unqualified name' do 18 | should eql('TestApp') 19 | end 20 | 21 | it_should_behave_like 'an idempotent method' 22 | end 23 | 24 | context 'with scoped constant name' do 25 | let(:raw_scope) { TestApp::Literal } 26 | 27 | it 'should return the unqualified name' do 28 | should eql('Literal') 29 | end 30 | 31 | it_should_behave_like 'an idempotent method' 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant do 4 | describe '.traverse' do 5 | def apply 6 | described_class.traverse(action, values) 7 | end 8 | 9 | let(:values) { [1, 2, 3] } 10 | 11 | context 'all evalauting to right' do 12 | let(:action) { ->(value) { right(value.to_s) } } 13 | let(:values) { [1, 2, 3] } 14 | 15 | it 'returns values' do 16 | expect(apply) 17 | .to eql(right(values.map(&:to_s))) 18 | end 19 | end 20 | 21 | context 'some evalauting to left' do 22 | 23 | let(:action) do 24 | lambda do |value| 25 | if value.equal?(2) 26 | left(2) 27 | else 28 | right(value) 29 | end 30 | end 31 | end 32 | 33 | it 'returns first left value' do 34 | expect(apply).to eql(left(2)) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast/pattern/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | class Pattern 6 | class Source 7 | include Anima.new(:string) 8 | 9 | def initialize(**attributes) 10 | super 11 | 12 | @lines = string.split("\n") 13 | end 14 | 15 | def line(line_index) 16 | @lines.fetch(line_index) 17 | end 18 | 19 | class Location 20 | include Anima.new(:source, :range, :line_index, :line_start) 21 | 22 | def display 23 | "#{source.line(line_index)}\n#{prefix}#{carets}" 24 | end 25 | 26 | private 27 | 28 | def prefix 29 | ' ' * (range.begin - line_start) 30 | end 31 | 32 | def carets 33 | '^' * range.size 34 | end 35 | end 36 | end # Source 37 | end # Pattern 38 | end # AST 39 | end # Mutant 40 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/named_value/variable_assignment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | module NamedValue 7 | 8 | # Mutation emitter to handle variable assignment nodes 9 | class VariableAssignment < Node 10 | 11 | children :name, :value 12 | 13 | map = { 14 | gvasgn: '$', 15 | cvasgn: '@@', 16 | ivasgn: '@', 17 | lvasgn: EMPTY_STRING 18 | } 19 | 20 | MAP = map 21 | .transform_values { |prefix| [prefix, /^#{::Regexp.escape(prefix)}/] } 22 | .freeze 23 | 24 | handle(*MAP.keys) 25 | 26 | private 27 | 28 | def dispatch 29 | emit_value_mutations if value # op asgn! 30 | end 31 | end # VariableAssignment 32 | end # NamedValue 33 | end # Node 34 | end # Mutator 35 | end # Mutant 36 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/sexp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Sexp do 4 | let(:object) do 5 | Class.new do 6 | include Mutant::AST::Sexp 7 | 8 | public :n_not 9 | public :s 10 | end.new 11 | end 12 | 13 | describe '#n_not' do 14 | subject { object.n_not(node) } 15 | 16 | let(:node) { s(:true) } 17 | 18 | it 'returns negated ast' do 19 | expect(subject).to eql(s(:send, s(:true), :!)) 20 | end 21 | end 22 | 23 | describe '#s' do 24 | subject { object.s(*arguments) } 25 | 26 | context 'with single argument' do 27 | let(:arguments) { %i[foo] } 28 | 29 | it { should eql(Parser::AST::Node.new(:foo)) } 30 | end 31 | 32 | context 'with single multiple arguments' do 33 | let(:arguments) { %i[foo bar baz] } 34 | 35 | it { should eql(Parser::AST::Node.new(:foo, %i[bar baz])) } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ruby/meta/regexp/regexp_zero_or_more.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :regexp_zero_or_more do 4 | source '/\d+/' 5 | 6 | singleton_mutations 7 | regexp_mutations 8 | 9 | mutation '/\D+/' 10 | mutation '/\d/' 11 | end 12 | 13 | Mutant::Meta::Example.add :regexp_greedy_zero_or_more do 14 | source '/\d*/' 15 | 16 | singleton_mutations 17 | regexp_mutations 18 | 19 | mutation '/\d/' 20 | mutation '/\d+/' 21 | mutation '/\D*/' 22 | end 23 | 24 | Mutant::Meta::Example.add :regexp_reluctant_zero_or_more do 25 | source '/\d*?/' 26 | 27 | singleton_mutations 28 | regexp_mutations 29 | 30 | mutation '/\d/' 31 | mutation '/\d+?/' 32 | mutation '/\D*?/' 33 | end 34 | 35 | Mutant::Meta::Example.add :regexp_possessive_zero_or_more do 36 | source '/\d*+/' 37 | 38 | singleton_mutations 39 | regexp_mutations 40 | 41 | mutation '/\d/' 42 | mutation '/\d++/' 43 | mutation '/\D*+/' 44 | end 45 | -------------------------------------------------------------------------------- /ruby/lib/mutant/parallel/pipe.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module Parallel 5 | class Pipe 6 | include Adamantium, Anima.new(:reader, :writer) 7 | 8 | # Run block with pipe in binmode 9 | # 10 | # @return [undefined] 11 | def self.with(io) 12 | io.pipe(binmode: true) do |(reader, writer)| 13 | yield new(reader:, writer:) 14 | end 15 | end 16 | 17 | def self.from_io(io) 18 | reader, writer = io.pipe(binmode: true) 19 | new(reader:, writer:) 20 | end 21 | 22 | # Writer end of the pipe 23 | # 24 | # @return [IO] 25 | def to_writer 26 | reader.close 27 | writer 28 | end 29 | 30 | # Parent reader end of the pipe 31 | # 32 | # @return [IO] 33 | def to_reader 34 | writer.close 35 | reader 36 | end 37 | end # Pipe 38 | end # Parallel 39 | end # Mutant 40 | -------------------------------------------------------------------------------- /ruby/mutant-minitest.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/mutant/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'mutant-minitest' 7 | gem.version = Mutant::VERSION.dup 8 | gem.authors = ['Markus Schirp'] 9 | gem.email = %w[mbj@schirp-dso.com] 10 | gem.description = 'Minitest integration for mutant' 11 | gem.summary = gem.description 12 | gem.homepage = 'https://github.com/mbj/mutant' 13 | gem.license = 'Nonstandard' 14 | 15 | gem.require_paths = %w[lib] 16 | gem.files = %w[lib/mutant/minitest/coverage.rb lib/mutant/integration/minitest.rb] 17 | 18 | gem.extra_rdoc_files = %w[LICENSE] 19 | 20 | gem.required_ruby_version = '>= 3.2' 21 | 22 | gem.metadata['rubygems_mfa_required'] = 'true' 23 | 24 | gem.add_dependency('minitest', '~> 5.11') 25 | gem.add_dependency('mutant', "= #{gem.version}") 26 | gem.add_dependency('mutex_m', '~> 0.2') 27 | end 28 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/range.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal 7 | 8 | # Abstract literal range mutator 9 | class Range < self 10 | 11 | MAP = { 12 | irange: :erange, 13 | erange: :irange 14 | }.freeze 15 | 16 | children :lower_bound, :upper_bound 17 | 18 | handle(*MAP.keys) 19 | 20 | private 21 | 22 | def dispatch 23 | emit_singletons 24 | emit_lower_bound_mutations if lower_bound 25 | 26 | return unless upper_bound 27 | 28 | emit_inverse 29 | emit_upper_bound_mutations 30 | end 31 | 32 | def emit_inverse 33 | emit(s(MAP.fetch(node.type), *children)) 34 | end 35 | 36 | end # Range 37 | end # Literal 38 | end # Node 39 | end # Mutator 40 | end # Mutant 41 | -------------------------------------------------------------------------------- /ruby/meta/or_asgn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :or_asgn do 4 | source 'a ||= 1' 5 | 6 | singleton_mutations 7 | mutation 'a ||= nil' 8 | mutation 'a ||= 0' 9 | mutation 'a ||= 2' 10 | end 11 | 12 | Mutant::Meta::Example.add :or_asgn do 13 | source '@a ||= 1' 14 | 15 | singleton_mutations 16 | mutation '@a ||= nil' 17 | mutation '@a ||= 0' 18 | mutation '@a ||= 2' 19 | end 20 | 21 | Mutant::Meta::Example.add :or_asgn do 22 | source 'Foo ||= nil' 23 | 24 | singleton_mutations 25 | end 26 | 27 | Mutant::Meta::Example.add :or_asgn do 28 | source '@a ||= self.bar' 29 | 30 | singleton_mutations 31 | mutation '@a ||= nil' 32 | mutation '@a ||= self' 33 | mutation '@a ||= bar' 34 | end 35 | 36 | Mutant::Meta::Example.add :or_asgn do 37 | source 'foo[:bar] ||= 1' 38 | 39 | singleton_mutations 40 | mutation 'foo[:bar] ||= nil' 41 | mutation 'foo[:bar] ||= 0' 42 | mutation 'foo[:bar] ||= 2' 43 | end 44 | -------------------------------------------------------------------------------- /ruby/meta/regexp/character_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | mutations = { 4 | [:regexp_digit_type, '/\d/'] => [:regexp_nondigit_type, '/\D/'], 5 | [:regexp_hex_type, '/\h/'] => [:regexp_nonhex_type, '/\H/'], 6 | [:regexp_space_type, '/\s/'] => [:regexp_nonspace_type, '/\S/'], 7 | [:regexp_word_boundary_anchor, '/\b/'] => [:regexp_nonword_boundary_anchor, '/\B/'], 8 | [:regexp_word_type, '/\w/'] => [:regexp_nonword_type, '/\W/'], 9 | [:regexp_xgrapheme_type, '/\X/'] => [:regexp_linebreak_type, '/\R/'] 10 | } 11 | 12 | mutations = mutations.merge(mutations.invert) 13 | 14 | mutations.each do |(source_type, source_mutation), (_, regexp_mutation)| 15 | Mutant::Meta::Example.add :regexp, source_type do 16 | source(source_mutation) 17 | 18 | singleton_mutations 19 | regexp_mutations 20 | 21 | mutation(regexp_mutation) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Mutator for array literals 8 | class Array < self 9 | 10 | handle(:array) 11 | 12 | children :first 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | emit_type 19 | mutate_body 20 | return unless children.one? 21 | emit(first) unless n_splat?(first) 22 | end 23 | 24 | def mutate_body 25 | children.each_index do |index| 26 | dup_children = children.dup 27 | dup_children.delete_at(index) 28 | emit_type(*dup_children) 29 | mutate_child(index) 30 | end 31 | end 32 | 33 | end # Array 34 | end # Literal 35 | end # Node 36 | end # Mutator 37 | end # Mutant 38 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'rspec integration', mutant: false do 4 | let(:base_cmd) do 5 | %w[ 6 | bundle exec mutant run 7 | --include lib 8 | --integration rspec 9 | --require test_app 10 | --usage opensource 11 | ] 12 | end 13 | 14 | %w[3.8 3.9 3.10 3.11 3.12 3.13].each do |version| 15 | context "RSpec #{version}" do 16 | let(:gemfile) { "Gemfile.rspec#{version}" } 17 | 18 | it_behaves_like 'framework integration' 19 | 20 | it 'handles invalid rspec' do 21 | Dir.chdir('test_app') do 22 | result = Kernel.system( 23 | { 'BUNDLE_GEMFILE' => gemfile }, 24 | *%w[bundle exec mutant environment test run --integration rspec -- --backtrace 25 | spec/unit/test_app/invalid.rb] 26 | ) 27 | 28 | expect(result).to be(false) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/pattern/source_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Pattern::Source do 4 | describe '#line' do 5 | def apply 6 | instance.line(line_index) 7 | end 8 | 9 | let(:string) { "a\n b" } 10 | 11 | let(:instance) do 12 | described_class.new(string:) 13 | end 14 | 15 | context 'on line index 0' do 16 | let(:line_index) { 0 } 17 | 18 | it 'returns first line' do 19 | expect(apply).to eql('a') 20 | end 21 | end 22 | 23 | context 'on line index 1' do 24 | let(:line_index) { 1 } 25 | 26 | it 'returns second line' do 27 | expect(apply).to eql(' b') 28 | end 29 | end 30 | 31 | context 'on non unix newline' do 32 | let(:string) { "a\r\nb" } 33 | let(:line_index) { 0 } 34 | 35 | it 'considers the \r as part of the previous line' do 36 | expect(apply).to eql("a\r") 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/matcher/chain_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Matcher::Chain, '#call' do 4 | subject { object.call(env) } 5 | 6 | let(:object) { described_class.new(matchers: [matcher_a, matcher_b]) } 7 | let(:env) { instance_double(Mutant::Env) } 8 | let(:matcher_a) { instance_double(Mutant::Matcher) } 9 | let(:matcher_b) { instance_double(Mutant::Matcher) } 10 | let(:subject_a) { instance_double(Mutant::Subject) } 11 | let(:subject_b) { instance_double(Mutant::Subject) } 12 | 13 | before do 14 | expect(matcher_a).to receive(:call) 15 | .with(env) 16 | .and_return([subject_a]) 17 | 18 | expect(matcher_b).to receive(:call) 19 | .with(env) 20 | .and_return([subject_a, subject_b]) 21 | end 22 | 23 | it 'returns concatenated unique matches' do 24 | should eql([subject_a, subject_b]) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ruby/lib/mutant/isolation/none.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Module providing isolation 5 | class Isolation 6 | # Absolutely no isolation 7 | # 8 | # Only useful for debugging. 9 | class None < self 10 | 11 | # Call block in no isolation 12 | # 13 | # @return [Result] 14 | # 15 | # rubocop:disable Lint/SuppressedException 16 | # rubocop:disable Metrics/MethodLength 17 | # ^^ it actually isn not suppressed, it assigns an lvar 18 | def call(_timeout) 19 | begin 20 | value = yield 21 | rescue => exception 22 | end 23 | 24 | Result.new( 25 | exception:, 26 | log: '', 27 | process_status: nil, 28 | timeout: nil, 29 | value: 30 | ) 31 | end 32 | # rubocop:enable Lint/SuppressedException 33 | # rubocop:enable Metrics/MethodLength 34 | 35 | end # None 36 | end # Isolation 37 | end # Mutant 38 | -------------------------------------------------------------------------------- /ruby/meta/procarg_zero.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :procarg0 do 4 | source 'foo { |a| }' 5 | 6 | singleton_mutations 7 | mutation 'foo { |a| raise }' 8 | mutation 'foo' 9 | end 10 | 11 | Mutant::Meta::Example.add :procarg0 do 12 | source 'foo { |(a)| }' 13 | 14 | singleton_mutations 15 | mutation 'foo { |(a)| raise }' 16 | mutation 'foo' 17 | end 18 | 19 | Mutant::Meta::Example.add :procarg0 do 20 | source 'foo { |(a, b)| }' 21 | 22 | singleton_mutations 23 | mutation 'foo { |a, b| }' 24 | mutation 'foo { |(a, b)| raise }' 25 | mutation 'foo' 26 | end 27 | 28 | Mutant::Meta::Example.add :procarg0 do 29 | source 'foo { |(*)| }' 30 | 31 | singleton_mutations 32 | mutation 'foo { |(*)| raise }' 33 | mutation 'foo' 34 | end 35 | 36 | Mutant::Meta::Example.add :procarg0 do 37 | source 'foo { |(a, (*))| }' 38 | 39 | singleton_mutations 40 | mutation 'foo { |a, (*)| }' 41 | mutation 'foo { |(a, (*))| raise }' 42 | mutation 'foo' 43 | end 44 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/meta/send/receiver_possible_top_level_const_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Meta::Send, '#receiver_possible_top_level_const?' do 4 | subject { described_class.new(node:).receiver_possible_top_level_const? } 5 | 6 | def parse(source) 7 | Unparser.parse(source) 8 | end 9 | 10 | context 'when implicit top level const' do 11 | let(:node) { parse('Foo.bar') } 12 | 13 | it { should be true } 14 | end 15 | 16 | context 'when cbase' do 17 | let(:node) { parse('::Foo.bar') } 18 | 19 | it { should be true } 20 | end 21 | 22 | context 'when nested const' do 23 | let(:node) { parse('Baz::Foo.bar') } 24 | 25 | it { should be false } 26 | end 27 | 28 | context 'when no receiver' do 29 | let(:node) { parse('bar') } 30 | 31 | it { should be false } 32 | end 33 | 34 | context 'when send receiver' do 35 | let(:node) { parse('foo.bar') } 36 | 37 | it { should be false } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /ruby/spec/shared/config_merge_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples 'maybe value merge' do 4 | context 'when original has value' do 5 | context 'when other does not have value' do 6 | let(:other_value) { nil } 7 | 8 | it 'sets value to original value' do 9 | expect_value(original_value) 10 | end 11 | end 12 | 13 | context 'when other does have a value' do 14 | it 'sets value to other value' do 15 | expect_value(other_value) 16 | end 17 | end 18 | end 19 | 20 | context 'when original does not have value' do 21 | let(:original_value) { nil } 22 | 23 | context 'when other does not have value' do 24 | let(:other_value) { nil } 25 | 26 | it 'sets value to nil value' do 27 | expect_value(nil) 28 | end 29 | end 30 | 31 | context 'when other does have a value' do 32 | it 'sets value to other value' do 33 | expect_value(other_value) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/argument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for required arguments 8 | class Argument < self 9 | handle(:arg, :kwarg) 10 | 11 | children :name 12 | 13 | private 14 | 15 | def dispatch; end 16 | 17 | # Mutator for optional arguments 18 | class Optional < self 19 | 20 | TYPE_MAP = { 21 | optarg: :arg, 22 | kwoptarg: :kwarg 23 | }.freeze 24 | 25 | handle(:optarg, :kwoptarg) 26 | 27 | children :name, :default 28 | 29 | private 30 | 31 | def dispatch 32 | emit_required_mutation 33 | emit_default_mutations 34 | end 35 | 36 | def emit_required_mutation 37 | emit(s(TYPE_MAP.fetch(node.type), name)) 38 | end 39 | 40 | end # Optional 41 | end # Argument 42 | end # Node 43 | end # Mutator 44 | end # Mutant 45 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/subject_result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::SubjectResult do 4 | setup_shared_context 5 | 6 | let(:reportable) { subject_a_result } 7 | 8 | describe '.call' do 9 | context 'on full coverage' do 10 | it_reports <<~'STR' 11 | subject-a 12 | - test-a 13 | STR 14 | end 15 | 16 | context 'on partial coverage' do 17 | with(:mutation_a_criteria_result) { { test_result: false } } 18 | 19 | it_reports <<~'STR' 20 | subject-a 21 | - test-a 22 | evil:subject-a:d27d2 23 | ----------------------- 24 | @@ -1 +1 @@ 25 | -true 26 | +false 27 | ----------------------- 28 | STR 29 | end 30 | 31 | context 'without results' do 32 | with(:subject_a_result) { { coverage_results: [] } } 33 | 34 | it_reports <<~'STR' 35 | subject-a 36 | - test-a 37 | STR 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /ruby/lib/mutant/ast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class AST 5 | include Adamantium, Anima.new( 6 | :node, 7 | :comment_associations 8 | ) 9 | 10 | class View 11 | include Adamantium, Anima.new(:node, :stack) 12 | end 13 | 14 | def on_line(line) 15 | line_map.fetch(line, EMPTY_HASH).map do |node, stack| 16 | View.new(node:, stack:) 17 | end 18 | end 19 | 20 | private 21 | 22 | def line_map 23 | line_map = {} 24 | 25 | walk_path(node, []) do |node, stack| 26 | expression = node.location.expression || next 27 | (line_map[expression.line] ||= []) << [node, stack] 28 | end 29 | 30 | line_map 31 | end 32 | memoize :line_map 33 | 34 | def walk_path(node, stack, &block) 35 | block.call(node, stack) 36 | stack = [*stack, node] 37 | node.children.grep(::Parser::AST::Node) do |child| 38 | walk_path(child, stack, &block) 39 | end 40 | end 41 | end # AST 42 | end # Mutant 43 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/named_value/constant_assignment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | module NamedValue 7 | 8 | # Mutation emitter to handle constant assignment nodes 9 | class ConstantAssignment < Node 10 | 11 | children :cbase, :name, :value 12 | 13 | handle :casgn 14 | 15 | private 16 | 17 | def dispatch 18 | mutate_name 19 | return unless value # op asgn 20 | emit_value_mutations 21 | emit_remove_const 22 | end 23 | 24 | def emit_remove_const 25 | emit(s(:send, cbase, :remove_const, s(:sym, name))) 26 | end 27 | 28 | def mutate_name 29 | Util::Symbol.call(input: name, parent: nil).each do |name| 30 | emit_name(name.upcase) 31 | end 32 | end 33 | 34 | end # ConstantAssignment 35 | end # NamedValue 36 | end # Node 37 | end # Mutator 38 | end # Mutant 39 | -------------------------------------------------------------------------------- /ruby/lib/mutant/expression/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Expression 5 | class Parser 6 | include Anima.new(:types) 7 | 8 | # Parse expression 9 | # 10 | # @param [String] input 11 | # 12 | # @return [Either] 13 | # if expression is valid 14 | # 15 | # @return [nil] 16 | # otherwise 17 | def call(input) 18 | case expressions(input) 19 | in [] 20 | Either::Left.new("Expression: #{input.inspect} is invalid") 21 | in [expression] 22 | Either::Right.new(expression) 23 | else 24 | Either::Left.new("Expression: #{input.inspect} is ambiguous") 25 | end 26 | end 27 | 28 | private 29 | 30 | def expressions(input) 31 | types.each_with_object([]) do |type, aggregate| 32 | expression = type.try_parse(input) and aggregate << expression 33 | end 34 | end 35 | 36 | end # Parser 37 | end # Expression 38 | end # Mutant 39 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/binary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutation emitter to handle binary connectives 7 | class Binary < self 8 | 9 | INVERSE = { 10 | and: :or, 11 | or: :and 12 | }.freeze 13 | 14 | handle(*INVERSE.keys) 15 | 16 | children :left, :right 17 | 18 | private 19 | 20 | def dispatch 21 | emit_singletons unless left_lvasgn? 22 | emit_promotions 23 | 24 | emit_left_mutations do |mutation| 25 | !(n_irange?(mutation) || n_erange?(mutation)) || !mutation.children.fetch(1).nil? 26 | end 27 | 28 | emit_right_mutations 29 | end 30 | 31 | def emit_promotions 32 | emit(left) 33 | emit(right) unless left_lvasgn? 34 | end 35 | 36 | def left_lvasgn? 37 | n_lvasgn?(left) 38 | end 39 | 40 | end # Binary 41 | end # Node 42 | end # Mutator 43 | end # Mutant 44 | -------------------------------------------------------------------------------- /mutant/src/main.rs: -------------------------------------------------------------------------------- 1 | use log::warn; 2 | use std::env; 3 | use std::os::unix::process::CommandExt; 4 | use std::process::Command; 5 | 6 | fn main() { 7 | env_logger::Builder::from_default_env() 8 | .filter_level(log::LevelFilter::Warn) 9 | .init(); 10 | 11 | let arguments: Vec = env::args().collect(); 12 | 13 | // For now, always delegate to Ruby with a warning 14 | warn!("Rust wrapper did not handle this command yet, delegating to Ruby implementation"); 15 | 16 | exec_ruby_mutant(&arguments[1..]); 17 | } 18 | 19 | fn exec_ruby_mutant(arguments: &[String]) { 20 | env::set_current_dir("ruby").unwrap_or_else(|error| { 21 | panic!("Failed to change to ruby directory: {}", error); 22 | }); 23 | 24 | let error = Command::new("bundle") 25 | .arg("exec") 26 | .arg("mutant") 27 | .args(arguments) 28 | .env("MUTANT_RUST", "1") 29 | .env("MUTANT_VERSION", env!("CARGO_PKG_VERSION")) 30 | .exec(); 31 | 32 | panic!("Failed to execute Ruby mutant: {}", error); 33 | } 34 | -------------------------------------------------------------------------------- /ruby/lib/mutant/cli/command/environment/subject.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module CLI 5 | class Command 6 | class Environment 7 | class Subject < self 8 | NAME = 'subject' 9 | SHORT_DESCRIPTION = 'Subject subcommands' 10 | 11 | class List < self 12 | NAME = 'list' 13 | SHORT_DESCRIPTION = 'List subjects' 14 | SUBCOMMANDS = EMPTY_ARRAY 15 | 16 | private 17 | 18 | def action 19 | bootstrap.fmap(&method(:list_subjects)) 20 | end 21 | 22 | def list_subjects(env) 23 | print('Subjects in environment: %d' % env.subjects.length) 24 | env.subjects.each do |subject| 25 | print(subject.expression.syntax) 26 | end 27 | end 28 | end 29 | 30 | SUBCOMMANDS = [List].freeze 31 | end # Subject 32 | end # Environment 33 | end # Command 34 | end # CLI 35 | end # Mutant 36 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/when.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for when nodes 8 | class When < self 9 | 10 | handle(:when) 11 | 12 | private 13 | 14 | def dispatch 15 | if body 16 | mutate_body 17 | else 18 | emit_child_update(body_index, N_RAISE) 19 | end 20 | mutate_conditions 21 | end 22 | 23 | def mutate_conditions 24 | conditions = children.length - 1 25 | children[..-2].each_index do |index| 26 | delete_child(index) if conditions > 1 27 | mutate_child(index) 28 | end 29 | end 30 | 31 | def mutate_body 32 | mutate_child(body_index) 33 | end 34 | 35 | def body 36 | children.fetch(body_index) 37 | end 38 | 39 | def body_index 40 | children.length - 1 41 | end 42 | 43 | end # When 44 | end # Node 45 | end # Mutator 46 | end # Mutant 47 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/sequence_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::Sequence do 4 | let(:object) { described_class.new(reporters: [reporter_a, reporter_b]) } 5 | let(:value) { instance_double(Object) } 6 | let(:reporter_a) { instance_double(Mutant::Reporter, delay: 1.0) } 7 | let(:reporter_b) { instance_double(Mutant::Reporter, delay: 2.0) } 8 | 9 | %i[report progress warn start].each do |name| 10 | describe "##{name}" do 11 | subject { object.public_send(name, value) } 12 | 13 | before do 14 | [reporter_a, reporter_b].each do |receiver| 15 | expect(receiver).to receive(name) 16 | .ordered 17 | .with(value) 18 | .and_return(receiver) 19 | end 20 | end 21 | 22 | it_should_behave_like 'a command method' 23 | end 24 | end 25 | 26 | describe '#delay' do 27 | it 'returns the lowest value' do 28 | expect(object.delay).to eql(1.0) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /docs/limitations.md: -------------------------------------------------------------------------------- 1 | Limitations 2 | =========== 3 | 4 | Subject 5 | ------- 6 | 7 | Mutant cannot emit mutations for some subjects. 8 | 9 | * methods defined within a closure. For example, methods defined using `module_eval`, `class_eval`, 10 | `define_method`, or `define_singleton_method`: 11 | 12 | ```ruby 13 | class Example 14 | class_eval do 15 | def example1 16 | end 17 | end 18 | 19 | module_eval do 20 | def example2 21 | end 22 | end 23 | 24 | define_method(:example3) do 25 | end 26 | 27 | define_singleton_method(:example4) do 28 | end 29 | end 30 | ``` 31 | 32 | * singleton methods not defined on a constant or `self` 33 | 34 | ```ruby 35 | class Foo 36 | def self.bar; end # ok 37 | def Foo.baz; end # ok 38 | 39 | myself = self 40 | def myself.qux; end # cannot mutate 41 | end 42 | ``` 43 | 44 | * methods defined with eval: 45 | 46 | ```ruby 47 | class Foo 48 | class_eval('def bar; end') # cannot mutate 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/send/binary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Send 7 | 8 | # Mutator for sends that correspond to a binary operator 9 | class Binary < self 10 | 11 | children :left, :operator, :right 12 | 13 | private 14 | 15 | def dispatch 16 | emit(left) 17 | emit_left_mutations 18 | emit_selector_replacement 19 | emit(right) 20 | emit_right_mutations 21 | emit_not_equality_mutations 22 | end 23 | 24 | def emit_not_equality_mutations 25 | return unless operator.equal?(:'!=') 26 | 27 | emit_not_equality_mutation(:eql?) 28 | emit_not_equality_mutation(:equal?) 29 | end 30 | 31 | def emit_not_equality_mutation(new_operator) 32 | emit(n_not(s(:send, left, new_operator, right))) 33 | end 34 | 35 | end # Binary 36 | 37 | end # Send 38 | end # Node 39 | end # Mutator 40 | end # Mutant 41 | -------------------------------------------------------------------------------- /ruby/lib/mutant/subject/method.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Subject 5 | # Abstract base class for method subjects 6 | class Method < self 7 | include anima.add(:visibility) 8 | 9 | # Method name 10 | # 11 | # @return [Expression] 12 | def name 13 | node.children.fetch(self.class::NAME_INDEX) 14 | end 15 | 16 | # Match expression 17 | # 18 | # @return [String] 19 | def expression 20 | Expression::Method.new( 21 | method_name: name.to_s, 22 | scope_symbol: self.class::SYMBOL, 23 | scope_name: scope.raw.name 24 | ) 25 | end 26 | memoize :expression 27 | 28 | # Match expressions 29 | # 30 | # @return [Array] 31 | def match_expressions 32 | [expression].concat(context.match_expressions) 33 | end 34 | memoize :match_expressions 35 | 36 | private 37 | 38 | def scope 39 | context.scope 40 | end 41 | 42 | end # Method 43 | end # Subject 44 | end # Mutant 45 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast/meta/send/proc_predicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST::Meta::Send, '#proc?' do 4 | subject { described_class.new(node:).proc? } 5 | 6 | shared_context 'proc send' do |source| 7 | let(:node) { Unparser.parse(source).children.first } 8 | end 9 | 10 | shared_examples 'proc definition' do |*args| 11 | include_context 'proc send', *args 12 | 13 | it { should be(true) } 14 | end 15 | 16 | shared_examples 'not a proc definition' do |*args| 17 | include_context 'proc send', *args 18 | 19 | it { should be_falsey } 20 | end 21 | 22 | it_behaves_like 'proc definition', 'proc { }' 23 | it_behaves_like 'proc definition', 'Proc.new { }' 24 | it_behaves_like 'not a proc definition', 'new { }' 25 | it_behaves_like 'not a proc definition', 'foo.proc { }' 26 | it_behaves_like 'not a proc definition', 'Proc.blah { }' 27 | it_behaves_like 'not a proc definition', 'Proc().new { }' 28 | it_behaves_like 'not a proc definition', 'Foo.new { }' 29 | it_behaves_like 'not a proc definition', 'blah { }' 30 | end 31 | -------------------------------------------------------------------------------- /ruby/lib/mutant/matcher/scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Matcher 5 | # Matcher expanding Mutant::Scope objects into method matches 6 | # at singleton or instance level 7 | # 8 | # If we *ever* get other subjects than methods, its likely the place 9 | # to hook in custom matchers. In that case the scope matchers to expand 10 | # should be passed as arguments to the constructor. 11 | class Scope < self 12 | include Anima.new(:scope) 13 | 14 | MATCHERS = [ 15 | Matcher::Methods::Singleton, 16 | Matcher::Methods::Instance, 17 | Matcher::Methods::Metaclass 18 | ].freeze 19 | 20 | private_constant(*constants(false)) 21 | 22 | # Matched subjects 23 | # 24 | # @param [Env] env 25 | # 26 | # @return [Enumerable] 27 | def call(env) 28 | Chain.new(matchers: effective_matchers).call(env) 29 | end 30 | 31 | private 32 | 33 | def effective_matchers 34 | MATCHERS.map { |matcher| matcher.new(scope:) } 35 | end 36 | 37 | end # Scope 38 | end # Matcher 39 | end # Mutant 40 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | 7 | # Mutator for case nodes 8 | class Case < self 9 | 10 | handle(:case) 11 | 12 | children :condition 13 | 14 | private 15 | 16 | def dispatch 17 | emit_singletons 18 | emit_condition_mutations if condition 19 | emit_when_mutations 20 | emit_else_mutations 21 | end 22 | 23 | def emit_when_mutations 24 | indices = children.each_index.drop(1).take(children.length - 2) 25 | one = indices.one? 26 | indices.each do |index| 27 | mutate_child(index) 28 | delete_child(index) unless one 29 | end 30 | end 31 | 32 | def emit_else_mutations 33 | else_branch = children.last 34 | else_index = children.length - 1 35 | return unless else_branch 36 | mutate_child(else_index) 37 | emit_child_update(else_index, nil) 38 | end 39 | 40 | end # Case 41 | end # Node 42 | end # Mutator 43 | end # Mutant 44 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/isolation/none_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Isolation::None do 4 | let(:timeout) { nil } 5 | 6 | describe '.call' do 7 | let(:object) { described_class.new } 8 | 9 | context 'without exception' do 10 | it 'returns success result' do 11 | expect(object.call(timeout) { :foo }).to eql( 12 | Mutant::Isolation::Result.new( 13 | exception: nil, 14 | log: '', 15 | process_status: nil, 16 | timeout: nil, 17 | value: :foo 18 | ) 19 | ) 20 | end 21 | end 22 | 23 | context 'with exception' do 24 | let(:exception) { RuntimeError.new('foo') } 25 | 26 | it 'returns error result' do 27 | expect(object.call(timeout) { fail exception }).to eql( 28 | Mutant::Isolation::Result.new( 29 | exception:, 30 | log: '', 31 | process_status: nil, 32 | timeout: nil, 33 | value: nil 34 | ) 35 | ) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /ruby/lib/mutant/meta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Namespace for mutant metadata 5 | module Meta 6 | require 'mutant/meta/example' 7 | require 'mutant/meta/example/dsl' 8 | require 'mutant/meta/example/verification' 9 | 10 | # Mutation example 11 | class Example 12 | 13 | # rubocop:disable Style/MutableConstant 14 | ALL = [] 15 | 16 | # Add example 17 | # 18 | # @return [undefined] 19 | def self.add(*types, operators: :full, &block) 20 | ALL << DSL.call( 21 | block:, 22 | location: caller_locations(1).first, 23 | operators: Mutation::Operators.parse(operators.to_s).from_right, 24 | types: Set.new(types) 25 | ) 26 | end 27 | 28 | Pathname.glob(Pathname.new(__dir__).parent.parent.join('meta', '*.rb')) 29 | .sort 30 | .each(&method(:require)) 31 | 32 | ALL.freeze 33 | 34 | # Remove mutation method only present for DSL executions from meta/**/*.rb 35 | class << self 36 | undef_method :add 37 | end 38 | 39 | end # Example 40 | end # Meta 41 | end # Mutant 42 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/named_value/access.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | module NamedValue 7 | 8 | # Mutation emitter to handle named value access nodes 9 | class Access < Node 10 | 11 | handle(:gvar, :cvar, :lvar, :self) 12 | 13 | private 14 | 15 | def dispatch 16 | emit_singletons 17 | end 18 | 19 | # Named value access emitter for instance variables 20 | class Ivar < Access 21 | NAME_RANGE = (1..-1) 22 | 23 | handle(:ivar) 24 | 25 | children :name 26 | 27 | private 28 | 29 | def dispatch 30 | emit_attribute_read 31 | super 32 | end 33 | 34 | def emit_attribute_read 35 | emit(s(:send, nil, attribute_name)) 36 | end 37 | 38 | def attribute_name 39 | name.slice(NAME_RANGE).to_sym 40 | end 41 | end # Ivar 42 | 43 | end # Access 44 | end # NamedValue 45 | end # Node 46 | end # Mutator 47 | end # Mutant 48 | -------------------------------------------------------------------------------- /ruby/lib/mutant/registry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Registry for mapping AST types to classes 5 | class Registry 6 | include Anima.new(:contents, :default) 7 | 8 | # Initialize object 9 | # 10 | # @return [undefined] 11 | def initialize(default) 12 | super(contents: {}, default:) 13 | end 14 | 15 | # Raised when the type is an invalid type 16 | RegistryError = Class.new(TypeError) 17 | 18 | # Register class for AST node class 19 | # 20 | # @param [Symbol] type 21 | # @param [Class] class 22 | # 23 | # @return [self] 24 | def register(type, klass) 25 | fail RegistryError, "Invalid type registration: #{type.inspect}" unless AST::Types::ALL.include?(type) 26 | fail RegistryError, "Duplicate type registration: #{type.inspect}" if contents.key?(type) 27 | contents[type] = klass 28 | self 29 | end 30 | 31 | # Lookup class for node 32 | # 33 | # @param [Symbol] type 34 | # 35 | # @return [Class] 36 | def lookup(type) 37 | contents.fetch(type, &default) 38 | end 39 | 40 | end # Registry 41 | end # Mutant 42 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/integration/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Integration::Config do 4 | describe '#merge' do 5 | let(:original_name) { 'original-name' } 6 | let(:other_name) { 'other-name' } 7 | 8 | subject do 9 | described_class.new( 10 | name: original_name, 11 | arguments: %w[original-argument] 12 | ) 13 | end 14 | 15 | def apply 16 | subject.merge(other) 17 | end 18 | 19 | let(:other) do 20 | described_class.new( 21 | name: other_name, 22 | arguments: %w[other-argument] 23 | ) 24 | end 25 | 26 | context 'when other name was present' do 27 | it 'uses other name' do 28 | expect(apply.name).to eql(other_name) 29 | end 30 | end 31 | 32 | context 'when other name was absent' do 33 | let(:other_name) { nil } 34 | it 'keeps original name' do 35 | expect(apply.name).to eql(original_name) 36 | end 37 | end 38 | 39 | it 'concatenates arguments' do 40 | expect(apply.arguments).to eql(%w[original-argument other-argument]) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/expression/descendants_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Expression::Descendants do 4 | let(:object) { parse_expression(input) } 5 | let(:input) { 'descendants:TestApp::Foo' } 6 | 7 | describe '#matcher' do 8 | def apply 9 | object.matcher(env: instance_double(Mutant::Env)) 10 | end 11 | 12 | it 'returns expected matcher' do 13 | expect(apply).to eql(Mutant::Matcher::Descendants.new(const_name: 'TestApp::Foo')) 14 | end 15 | end 16 | 17 | describe '#syntax' do 18 | def apply 19 | object.syntax 20 | end 21 | 22 | it 'returns input' do 23 | expect(apply).to eql(input) 24 | end 25 | end 26 | 27 | describe '.try_parse' do 28 | def apply 29 | described_class.try_parse(input) 30 | end 31 | 32 | context 'on valid input' do 33 | it 'returns expected matcher' do 34 | expect(apply).to eql(described_class.new(const_name: 'TestApp::Foo')) 35 | end 36 | end 37 | 38 | context 'on invalid input' do 39 | let(:input) { '' } 40 | 41 | it 'returns nil' do 42 | expect(apply).to be(nil) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/mutator/regexp/registry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Mutator::Regexp::Registry, mutant_expression: 'Mutant::Mutator*' do 4 | describe '.register' do 5 | let(:object) do 6 | described_class.new 7 | end 8 | 9 | let(:expression_class) { Class.new } 10 | let(:mutator_class_a) { Class.new } 11 | let(:mutator_class_b) { Class.new } 12 | 13 | def apply 14 | object.register(expression_class, mutator_class_a) 15 | end 16 | 17 | it 'returns self' do 18 | expect(apply).to be(object) 19 | end 20 | 21 | it 'registers single class' do 22 | apply 23 | 24 | expect(object.lookup(expression_class)).to eql( 25 | [ 26 | mutator_class_a, 27 | Mutant::Mutator::Regexp::Quantifier 28 | ] 29 | ) 30 | end 31 | 32 | it 'registers multiple classes' do 33 | object.register(expression_class, mutator_class_b) 34 | apply 35 | 36 | expect(object.lookup(expression_class)).to eql( 37 | [ 38 | mutator_class_b, 39 | mutator_class_a, 40 | Mutant::Mutator::Regexp::Quantifier 41 | ] 42 | ) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/exception_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Exception do 4 | subject { described_class.new(error_class:, block:) } 5 | 6 | let(:error_class) do 7 | Class.new(RuntimeError) 8 | end 9 | 10 | describe '#call' do 11 | def apply 12 | subject.call(input) 13 | end 14 | 15 | let(:input) { 2 } 16 | 17 | context 'block that does not raise' do 18 | let(:block) { ->(input) { input * input } } 19 | 20 | it 'returns expected success value' do 21 | expect(apply).to eql(Mutant::Either::Right.new(4)) 22 | end 23 | end 24 | 25 | context 'on block that raises' do 26 | context 'a covered exception' do 27 | let(:block) { ->(_input) { fail(error_class, 'some message') } } 28 | 29 | let(:error) do 30 | Mutant::Transform::Error.new( 31 | cause: nil, 32 | input:, 33 | message: 'some message', 34 | transform: subject 35 | ) 36 | end 37 | 38 | it 'returns expected error' do 39 | expect(apply).to eql(Mutant::Either::Left.new(error)) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ruby/lib/mutant/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Loader 5 | include Anima.new(:binding, :kernel, :source, :subject) 6 | 7 | FROZEN_STRING_FORMAT = "# frozen_string_literal: true\n%s" 8 | VOID_VALUE_REGEXP = /\A[^:]+:\d+: void value expression/ 9 | 10 | private_constant(*constants(false)) 11 | 12 | VOID_VALUE = Either::Left.new(nil) 13 | SUCCESS = Either::Right.new(nil) 14 | 15 | # Call loader 16 | # 17 | # @return [Result] 18 | def self.call(*) 19 | new(*).call 20 | end 21 | 22 | # Call loader 23 | # 24 | # One off the very few valid uses of eval ever. 25 | # 26 | # @return [Result] 27 | # 28 | # rubocop:disable Metrics/MethodLength 29 | def call 30 | kernel.eval( 31 | FROZEN_STRING_FORMAT % source, 32 | binding, 33 | subject.source_path.to_s, 34 | subject.source_line 35 | ) 36 | rescue SyntaxError => exception 37 | # rubocop:disable Style/GuardClause 38 | if VOID_VALUE_REGEXP.match?(exception.message) 39 | VOID_VALUE 40 | else 41 | raise 42 | end 43 | else 44 | SUCCESS 45 | end 46 | end # Loader 47 | end # Mutant 48 | -------------------------------------------------------------------------------- /ruby/lib/mutant/scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Class or Module bound to an exact expression 5 | class Scope 6 | include Adamantium, Anima.new(:raw, :expression) 7 | 8 | NAMESPACE_DELIMITER = '::' 9 | 10 | # Nesting of scope 11 | # 12 | # @return [Enumerable] 13 | def nesting 14 | const = Object 15 | name_nesting.map do |name| 16 | const = const.const_get(name) 17 | end 18 | end 19 | memoize :nesting 20 | 21 | # Unqualified name of scope 22 | # 23 | # @return [String] 24 | def unqualified_name 25 | name_nesting.last 26 | end 27 | 28 | # Match expressions for scope 29 | # 30 | # @return [Enumerable] 31 | def match_expressions 32 | name_nesting.each_index.reverse_each.map do |index| 33 | Expression::Namespace::Recursive.new( 34 | scope_name: name_nesting.take(index.succ).join(NAMESPACE_DELIMITER) 35 | ) 36 | end 37 | end 38 | memoize :match_expressions 39 | 40 | private 41 | 42 | def name_nesting 43 | raw.name.split(NAMESPACE_DELIMITER) 44 | end 45 | memoize :name_nesting 46 | 47 | end # Scope 48 | end # Mutant 49 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/result/coverage_criteria_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Result::CoverageCriteria do 4 | let(:object) do 5 | described_class.new( 6 | process_abort:, 7 | test_result:, 8 | timeout: 9 | ) 10 | end 11 | 12 | let(:timeout) { false } 13 | let(:test_result) { false } 14 | let(:process_abort) { false } 15 | 16 | describe '#success?' do 17 | def apply 18 | object.success? 19 | end 20 | 21 | context 'on no success criteria set' do 22 | it 'returns false' do 23 | expect(apply).to be(false) 24 | end 25 | end 26 | 27 | context 'on timeout criteria set' do 28 | let(:timeout) { true } 29 | 30 | it 'returns true' do 31 | expect(apply).to be(true) 32 | end 33 | end 34 | 35 | context 'on test result criteria set' do 36 | let(:test_result) { true } 37 | 38 | it 'returns true' do 39 | expect(apply).to be(true) 40 | end 41 | end 42 | 43 | context 'on process_abort criteria set' do 44 | let(:process_abort) { true } 45 | 46 | it 'returns true' do 47 | expect(apply).to be(true) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/named_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Named do 4 | subject { described_class.new(name:, transform:) } 5 | 6 | let(:name) { 'transform-name' } 7 | let(:transform) { Mutant::Transform::Boolean.new } 8 | 9 | describe '#slug' do 10 | def apply 11 | subject.slug 12 | end 13 | 14 | it 'returns name' do 15 | expect(apply).to be(name) 16 | end 17 | end 18 | 19 | describe '#call' do 20 | def apply 21 | subject.call(input) 22 | end 23 | 24 | context 'on valid input' do 25 | let(:input) { true } 26 | 27 | it 'returns sucess' do 28 | expect(apply).to eql(Mutant::Either::Right.new(input)) 29 | end 30 | end 31 | 32 | context 'on invalid input' do 33 | let(:input) { 1 } 34 | 35 | let(:error) do 36 | Mutant::Transform::Error.new( 37 | cause: transform.call(input).from_left, 38 | input:, 39 | message: nil, 40 | transform: subject 41 | ) 42 | end 43 | 44 | it 'returns failure' do 45 | expect(apply).to eql(Mutant::Either::Left.new(error)) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal 7 | # Mutator for hash literals 8 | class Hash < self 9 | 10 | handle(:hash) 11 | 12 | private 13 | 14 | def dispatch 15 | emit_singletons 16 | emit_type 17 | mutate_body 18 | end 19 | 20 | def mutate_body 21 | children.each_index do |index| 22 | mutate_child(index) 23 | dup_children = children.dup 24 | dup_children.delete_at(index) 25 | emit_type(*dup_children) 26 | end 27 | end 28 | 29 | # Mutator for hash pairs 30 | class Pair < Node 31 | 32 | handle(:pair) 33 | 34 | children :key, :value 35 | 36 | private 37 | 38 | def dispatch 39 | emit_key_mutations do |mutation| 40 | !mutation.eql?(s(:nil)) 41 | end 42 | emit_value_mutations 43 | end 44 | 45 | end # Pair 46 | end # Hash 47 | end # Literal 48 | end # Node 49 | end # Mutator 50 | end # Mutant 51 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Printer for mutation config 8 | class Config < self 9 | 10 | # Report configuration 11 | # 12 | # @param [Mutant::Config] config 13 | # 14 | # @return [undefined] 15 | # 16 | # rubocop:disable Metrics/AbcSize 17 | def run 18 | info 'Usage: %s', object.usage.value 19 | info 'Matcher: %s', object.matcher.inspect 20 | info 'Integration: %s', object.integration.name || 'null' 21 | info 'Jobs: %s', object.jobs || 'auto' 22 | info 'Includes: %s', object.includes 23 | info 'Requires: %s', object.requires 24 | info 'Operators: %s', object.mutation.operators.class.operators_name 25 | info 'MutationTimeout: %0.9g', object.mutation.timeout if object.mutation.timeout 26 | end 27 | # rubocop:enable Metrics/AbcSize 28 | 29 | end # Config 30 | end # Printer 31 | end # CLI 32 | end # Reporter 33 | end # Mutant 34 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/kwargs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for kwargs node 7 | class Kwargs < self 8 | 9 | DISALLOW = %i[nil self].freeze 10 | 11 | private_constant(*constants(false)) 12 | 13 | handle(:kwargs) 14 | 15 | private 16 | 17 | def dispatch 18 | emit_argument_presence 19 | emit_argument_mutations 20 | end 21 | 22 | def emit_argument_presence 23 | Util::Array::Presence.call(input: children, parent: nil).each do |children| 24 | emit_type(*children) unless children.empty? 25 | end 26 | end 27 | 28 | def emit_argument_mutations 29 | children.each_with_index do |child, index| 30 | mutate(node: child).each do |mutant| 31 | unless forbid_argument?(mutant) 32 | emit_child_update(index, mutant) 33 | end 34 | end 35 | end 36 | end 37 | 38 | def forbid_argument?(node) 39 | n_pair?(node) && DISALLOW.include?(node.children.first.type) 40 | end 41 | end # Kwargs 42 | end # Node 43 | end # Mutator 44 | end # Mutant 45 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/expression_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Expression do 4 | describe '#prefix?' do 5 | let(:object) { parse_expression('Foo*') } 6 | 7 | subject { object.prefix?(other) } 8 | 9 | context 'when object is a prefix of other' do 10 | let(:other) { parse_expression('Foo::Bar') } 11 | 12 | it { should be(true) } 13 | end 14 | 15 | context 'when other is not a prefix of other' do 16 | let(:other) { parse_expression('Bar') } 17 | 18 | it { should be(false) } 19 | end 20 | end 21 | 22 | describe '#frozen?' do 23 | subject { parse_expression('Foo').frozen? } 24 | 25 | it { should be(true) } 26 | end 27 | 28 | describe '.try_parse' do 29 | let(:object) do 30 | Class.new(described_class) do 31 | include Unparser::Anima.new(:foo) 32 | 33 | const_set(:REGEXP, /(?foo)/) 34 | end 35 | end 36 | 37 | subject { object.try_parse(input) } 38 | 39 | context 'good input' do 40 | let(:input) { 'foo' } 41 | 42 | it { should eql(object.new(foo: 'foo')) } 43 | end 44 | 45 | context 'bad input' do 46 | let(:input) { 'bar' } 47 | 48 | it { should be(nil) } 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /ruby/lib/mutant/expression/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Expression 5 | class Source < self 6 | include Anima.new(:glob_expression) 7 | 8 | REGEXP = /\Asource:(?.+)\z/ 9 | 10 | def syntax 11 | "source:#{glob_expression}" 12 | end 13 | 14 | def matcher(env:) 15 | Matcher::Chain.new(matchers: find_matchers(env:)) 16 | end 17 | 18 | private 19 | 20 | def find_matchers(env:) 21 | scope_names(env:).uniq.map do |scope_name| 22 | Namespace::Recursive.new(scope_name:).matcher(env: nil) 23 | end 24 | end 25 | 26 | def scope_names(env:) 27 | env.world.pathname.glob(glob_expression).flat_map do |path| 28 | toplevel_consts(env.parser.call(path).node).map(&Unparser.public_method(:unparse)) 29 | end 30 | end 31 | 32 | def toplevel_consts(node) 33 | children = node.children 34 | 35 | case node.type 36 | when :class, :module 37 | [children.fetch(0)] 38 | when :begin 39 | children.flat_map(&method(__method__)) 40 | else 41 | EMPTY_ARRAY 42 | end 43 | end 44 | end # Source 45 | end # Expression 46 | end # Mutant 47 | -------------------------------------------------------------------------------- /ruby/lib/mutant/repository/diff/ranges.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | module Repository 5 | class Diff 6 | module Ranges 7 | DECIMAL = /(?:0|[1-9]\d*)/ 8 | REGEXP = /\A@@ -(#{DECIMAL})(?:,(#{DECIMAL}))? \+(#{DECIMAL})(?:,(#{DECIMAL}))? @@/ 9 | 10 | private_constant(*constants(false)) 11 | 12 | # Parse a unified diff into ranges 13 | # 14 | # @param [String] 15 | # 16 | # @return [Set>] 17 | def self.parse(diff) 18 | diff.lines.flat_map(&method(:parse_ranges)).to_set 19 | end 20 | 21 | def self.parse_ranges(line) 22 | match = REGEXP.match(line) or return EMPTY_ARRAY 23 | 24 | match 25 | .captures 26 | .each_slice(2) 27 | .map { |start, offset| mk_range(start, offset) } 28 | .reject { |range| range.end < range.begin } 29 | end 30 | private_class_method :parse_ranges 31 | 32 | def self.mk_range(start, offset) 33 | start = Integer(start) 34 | 35 | ::Range.new(start, start + (offset ? Integer(offset).pred : 0)) 36 | end 37 | private_class_method :mk_range 38 | end # Ranges 39 | end # Diff 40 | end # Repository 41 | end # Ranges 42 | -------------------------------------------------------------------------------- /ruby/lib/mutant/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # An abstract context where mutations can be applied to. 5 | class Context 6 | include Adamantium, Anima.new(:constant_scope, :scope, :source_path) 7 | 8 | class ConstantScope 9 | include AST::Sexp 10 | 11 | class Class < self 12 | include Anima.new(:const, :descendant) 13 | 14 | def call(node) 15 | s(:class, const, nil, descendant.call(node)) 16 | end 17 | end 18 | 19 | class Module < self 20 | include Anima.new(:const, :descendant) 21 | 22 | def call(node) 23 | s(:module, const, descendant.call(node)) 24 | end 25 | end 26 | 27 | class None < self 28 | include Equalizer.new 29 | 30 | def call(node) 31 | node 32 | end 33 | end 34 | end 35 | 36 | def match_expressions 37 | scope.match_expressions 38 | end 39 | 40 | # Return root node for mutation 41 | # 42 | # @return [Parser::AST::Node] 43 | def root(node) 44 | constant_scope.call(node) 45 | end 46 | 47 | # Identification string 48 | # 49 | # @return [String] 50 | def identification 51 | scope.raw.name 52 | end 53 | 54 | end # Context 55 | end # Mutant 56 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/ast_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::AST do 4 | let(:object) { described_class.new(comment_associations: [], node:) } 5 | 6 | describe '#on_line' do 7 | def apply(line) 8 | object.on_line(line) 9 | end 10 | 11 | let(:node) do 12 | Unparser.parse(<<~RUBY) 13 | 14 | def foo 15 | begin 16 | 1; 2 17 | end 18 | end 19 | RUBY 20 | end 21 | 22 | context 'unpopulated line' do 23 | it 'returns empty view' do 24 | expect(apply(1)).to eql([]) 25 | end 26 | end 27 | 28 | context 'line populated with one node' do 29 | it 'returns expected view' do 30 | expect(apply(2)).to eql( 31 | [ 32 | described_class::View.new(node:, stack: []) 33 | ] 34 | ) 35 | end 36 | end 37 | 38 | context 'line populated with more than one' do 39 | it 'returns expected view' do 40 | expect(apply(4)).to eql( 41 | [ 42 | described_class::View.new(node: s(:int, 1), stack: [node, node.children.fetch(2)]), 43 | described_class::View.new(node: s(:int, 2), stack: [node, node.children.fetch(2)]) 44 | ] 45 | ) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ruby/lib/mutant/subject/method/instance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Subject 5 | class Method 6 | # Instance method subjects 7 | class Instance < self 8 | 9 | NAME_INDEX = 0 10 | SYMBOL = '#' 11 | 12 | # Prepare subject for mutation insertion 13 | # 14 | # @return [self] 15 | def prepare 16 | scope.raw.undef_method(name) 17 | self 18 | end 19 | 20 | def post_insert 21 | scope.raw.__send__(visibility, name) 22 | self 23 | end 24 | 25 | # Mutator for memoizable memoized instance methods 26 | class Memoized < self 27 | include AST::Sexp 28 | 29 | # Prepare subject for mutation insertion 30 | # 31 | # @return [self] 32 | def prepare 33 | scope 34 | .raw 35 | .instance_variable_get(:@memoized_methods) 36 | .delete(name) 37 | 38 | super 39 | end 40 | 41 | private 42 | 43 | def wrap_node(mutant) 44 | s(:begin, mutant, s(:send, nil, :memoize, s(:sym, name))) 45 | end 46 | end # Memoized 47 | end # Instance 48 | end # Method 49 | end # Subject 50 | end # Mutant 51 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/test/status_progressive_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::Test::StatusProgressive do 4 | setup_shared_context 5 | 6 | let(:reportable) { test_status } 7 | let(:test_results) { [] } 8 | 9 | let(:test_env) do 10 | Mutant::Result::TestEnv.new( 11 | env:, 12 | runtime: 0.8, 13 | test_results: 14 | ) 15 | end 16 | 17 | let(:test_status) do 18 | Mutant::Parallel::Status.new( 19 | active_jobs: 1, 20 | done: false, 21 | payload: test_env 22 | ) 23 | end 24 | 25 | describe '.call' do 26 | context 'with empty scheduler' do 27 | it_reports <<~REPORT 28 | progress: 00/02 failed: 0 runtime: 0.80s testtime: 0.00s tests/s: 0.00 29 | REPORT 30 | end 31 | 32 | context 'with test results' do 33 | let(:test_results) do 34 | [ 35 | Mutant::Result::Test.new( 36 | job_index: 0, 37 | output: '', 38 | passed: false, 39 | runtime: 0.5 40 | ) 41 | ] 42 | end 43 | 44 | it_reports <<~REPORT 45 | progress: 01/02 failed: 1 runtime: 0.80s testtime: 0.50s tests/s: 1.25 46 | REPORT 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ruby/lib/mutant/minitest/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest' 4 | 5 | module Mutant 6 | module Minitest 7 | module Coverage 8 | # Setup coverage declaration for current class 9 | # 10 | # @param [String] 11 | # 12 | # @example 13 | # 14 | # class MyTest < MiniTest::Test 15 | # cover 'MyCode*' 16 | # 17 | # def test_some_stuff 18 | # end 19 | # end 20 | # 21 | # @api public 22 | def cover(expression) 23 | @cover_expressions = Set.new unless defined?(@cover_expressions) 24 | 25 | @cover_expressions << expression 26 | end 27 | 28 | # Effective coverage expression 29 | # 30 | # @return [Set] 31 | # 32 | # @api private 33 | def resolve_cover_expressions 34 | return @cover_expressions if defined?(@cover_expressions) 35 | 36 | try_superclass_cover_expressions 37 | end 38 | 39 | private 40 | 41 | def try_superclass_cover_expressions 42 | return if superclass.equal?(::Minitest::Runnable) 43 | 44 | superclass.resolve_cover_expressions 45 | end 46 | 47 | end # Coverage 48 | end # Minitest 49 | end # Mutant 50 | 51 | Minitest::Test.extend(Mutant::Minitest::Coverage) 52 | -------------------------------------------------------------------------------- /ruby/spec/shared/method_matcher_behavior.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples_for 'a method matcher' do 4 | let(:node) { mutation_subject.node } 5 | let(:context) { mutation_subject.context } 6 | let(:mutation_subject) { subject.first } 7 | 8 | it 'returns one subject' do 9 | expect(subject.size).to be(1) 10 | end 11 | 12 | it 'has expected method name' do 13 | expect(name).to eql(method_name) 14 | end 15 | 16 | it 'has expected line number' do 17 | expect(node.location.expression.line).to eql(method_line) 18 | end 19 | 20 | it 'has expected arity' do 21 | expect(arguments.children.length).to eql(method_arity) 22 | end 23 | 24 | it 'has expected scope in context' do 25 | expect(context.scope).to eql(scope) 26 | end 27 | 28 | it 'has source path in context' do 29 | expect(context.source_path).to eql(source_path) 30 | end 31 | 32 | it 'has the correct node type' do 33 | expect(node.type).to be(type) 34 | end 35 | end 36 | 37 | RSpec.shared_examples_for 'skipped candidate' do 38 | before do 39 | expected_warnings.each do |warning| 40 | expect(env).to receive(:warn).with(warning).and_return(env) 41 | end 42 | end 43 | 44 | it 'does not emit matcher' do 45 | expect(subject).to eql([]) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/literal/regex.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | class Literal < self 7 | # Mutator for regexp literals 8 | class Regex < self 9 | 10 | handle(:regexp) 11 | 12 | # No input can ever be matched with this 13 | NULL_REGEXP_SOURCE = 'nomatch\A' 14 | 15 | private 16 | 17 | def options 18 | children.last 19 | end 20 | 21 | def dispatch 22 | mutate_body 23 | emit_singletons unless parent_node 24 | children.each_with_index do |child, index| 25 | mutate_child(index) unless n_str?(child) 26 | end 27 | emit_type(options) 28 | emit_type(s(:str, NULL_REGEXP_SOURCE), options) 29 | end 30 | 31 | def mutate_body 32 | string = Regexp.regexp_body(input) or return 33 | 34 | Regexp 35 | .mutate(::Regexp::Parser.parse(string)) 36 | .each(&method(:emit_regexp_body)) 37 | end 38 | 39 | def emit_regexp_body(expression) 40 | emit_type(s(:str, expression.to_str), options) 41 | end 42 | end # Regexp 43 | end # Literal 44 | end # Node 45 | end # Mutator 46 | end # Mutant 47 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::EnvResult do 4 | setup_shared_context 5 | 6 | with(:mutation_a_criteria_result) { { test_result: false } } 7 | 8 | let(:reportable) { env_result } 9 | 10 | describe '.call' do 11 | it_reports <<~'STR' 12 | subject-a 13 | - test-a 14 | evil:subject-a:d27d2 15 | ----------------------- 16 | @@ -1 +1 @@ 17 | -true 18 | +false 19 | ----------------------- 20 | Mutant environment: 21 | Usage: unknown 22 | Matcher: # 23 | Integration: null 24 | Jobs: auto 25 | Includes: [] 26 | Requires: [] 27 | Operators: light 28 | MutationTimeout: 5 29 | Subjects: 1 30 | All-Tests: 2 31 | Available-Tests: 1 32 | Selected-Tests: 1 33 | Tests/Subject: 1.00 avg 34 | Mutations: 2 35 | Results: 2 36 | Kills: 1 37 | Alive: 1 38 | Timeouts: 0 39 | Runtime: 4.00s 40 | Killtime: 2.00s 41 | Efficiency: 50.00% 42 | Mutations/s: 0.50 43 | Coverage: 50.00% 44 | STR 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /ruby/meta/block_pass.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :block_pass do 4 | source 'foo(&bar)' 5 | 6 | singleton_mutations 7 | mutation 'foo' 8 | mutation 'foo(&nil)' 9 | end 10 | 11 | Mutant::Meta::Example.add :block_pass do 12 | source 'foo(&method(:bar))' 13 | 14 | singleton_mutations 15 | mutation 'foo' 16 | mutation 'foo(&nil)' 17 | mutation 'foo(&method)' 18 | mutation 'foo(&method(nil))' 19 | mutation 'foo(&method(:bar__mutant__))' 20 | mutation 'foo(&public_method(:bar))' 21 | mutation 'foo(&:bar)' 22 | end 23 | 24 | Mutant::Meta::Example.add :block_pass do 25 | source 'foo(&:to_s)' 26 | 27 | singleton_mutations 28 | mutation 'foo' 29 | mutation 'foo(&nil)' 30 | mutation 'foo(&:to_str)' 31 | mutation 'foo(&:to_s__mutant__)' 32 | end 33 | 34 | Mutant::Meta::Example.add :block_pass do 35 | source 'foo(&:bar)' 36 | 37 | singleton_mutations 38 | mutation 'foo' 39 | mutation 'foo(&nil)' 40 | mutation 'foo(&:bar__mutant__)' 41 | end 42 | 43 | if RUBY_VERSION >= '3.1' 44 | Mutant::Meta::Example.add :block_pass do 45 | source 'def foo(a, &); foo(&); end' 46 | 47 | mutation 'def foo(a, &); end' 48 | mutation 'def foo(a, &); foo; end' 49 | mutation 'def foo(a, &); nil; end' 50 | mutation 'def foo(a, &); raise; end' 51 | mutation 'def foo(a, &); super; end' 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/primitive_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Primitive do 4 | subject { described_class.new(primitive:) } 5 | 6 | let(:primitive) { String } 7 | 8 | describe '#slug' do 9 | def apply 10 | subject.slug 11 | end 12 | 13 | it 'returns strigified primitive' do 14 | expect(apply).to eql('String') 15 | end 16 | 17 | it 'is idempotent' do 18 | expect(apply).to be(apply) 19 | end 20 | 21 | it 'is frozen' do 22 | expect(apply.frozen?).to be(true) 23 | end 24 | end 25 | 26 | describe '#call' do 27 | def apply 28 | subject.call(input) 29 | end 30 | 31 | context 'on string input' do 32 | let(:input) { 'some-string' } 33 | 34 | it 'returns sucess' do 35 | expect(apply).to eql(Mutant::Either::Right.new(input)) 36 | end 37 | end 38 | 39 | context 'on other input' do 40 | let(:input) { 1 } 41 | 42 | let(:error) do 43 | Mutant::Transform::Error.new( 44 | cause: nil, 45 | input:, 46 | message: 'Expected: String but got: Integer', 47 | transform: subject 48 | ) 49 | end 50 | 51 | it 'returns failure' do 52 | expect(apply).to eql(Mutant::Either::Left.new(error)) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /docs/sorbet.md: -------------------------------------------------------------------------------- 1 | ### Sorbet 2 | 3 | Mutant aims to implement full sorbet support. Full support includes: 4 | 5 | * Being able to mutate code that has sorbet signatures, as of mutant-0.11.0. 6 | * Being able to cover mutations via the type system. This capability is being 7 | developed right now. 8 | 9 | ### Setup 10 | 11 | There is currently no special setup required on a sorbet enabled code base. 12 | Mutant will automatically "see through" code with signatures and its type check 13 | wrapping to act on the underlying method directly. 14 | 15 | With adding the type system coverage feature special setup may or may not be 16 | required. 17 | 18 | ### Sorbet version support 19 | 20 | At this time mutant supports sorbet versions `0.5.x`, which is right now the latest 21 | available release branch. While sorbet is being stabilized the version support 22 | targets are fluent. Its the goal to support sorbet versions that mutants user-base needs. 23 | This currently means: At a minimum the latest release branch, and all branches that are not 24 | older than 6 month. 25 | 26 | As mutants sorbet support is new, this policy may be amended based on user feedback. 27 | 28 | ### Non sorbet target projects 29 | 30 | While mutant itself depends on `sorbet-runtime` it works with code bases that do not 31 | use sorbet. Please reach out if the presence of `sorbet-runtime` causes issues for 32 | your project. 33 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | # Abstract base class for reporters 5 | class Reporter 6 | include AbstractType 7 | 8 | # Write warning message 9 | # 10 | # @param [String] message 11 | # 12 | # @return [self] 13 | abstract_method :warn 14 | 15 | # Report start 16 | # 17 | # @param [Env] env 18 | # 19 | # @return [self] 20 | abstract_method :start 21 | 22 | # Report test start 23 | # 24 | # @param [Env] env 25 | # 26 | # @return [self] 27 | abstract_method :test_start 28 | 29 | # Report final state 30 | # 31 | # @param [Runner::Collector] collector 32 | # 33 | # @return [self] 34 | abstract_method :report 35 | 36 | # Report final test state 37 | # 38 | # @param [Runner::Collector] collector 39 | # 40 | # @return [self] 41 | abstract_method :test_report 42 | 43 | # Report progress on object 44 | # 45 | # @param [Object] object 46 | # 47 | # @return [self] 48 | abstract_method :progress 49 | 50 | # Report progress on object 51 | # 52 | # @param [Object] object 53 | # 54 | # @return [self] 55 | abstract_method :test_progress 56 | 57 | # The reporter delay 58 | # 59 | # @return [Float] 60 | abstract_method :delay 61 | 62 | end # Reporter 63 | end # Mutant 64 | -------------------------------------------------------------------------------- /ruby/spec/support/file_system.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MutantSpec 4 | class FileState 5 | DEFAULTS = { 6 | file: false, 7 | contents: nil, 8 | requires: [].freeze 9 | }.freeze 10 | 11 | include Adamantium, Anima.new(*DEFAULTS.keys) 12 | 13 | def self.new(attributes = DEFAULTS) 14 | super(DEFAULTS.merge(attributes)) 15 | end 16 | 17 | DOES_NOT_EXIST = new 18 | 19 | alias_method :file?, :file 20 | end # FileState 21 | 22 | class FakePathname 23 | include Adamantium, Anima.new(:file_system, :pathname) 24 | 25 | def join(*) 26 | self.class.new( 27 | file_system:, 28 | pathname: pathname.join(*) 29 | ) 30 | end 31 | 32 | def read 33 | state.contents 34 | end 35 | 36 | def to_s 37 | pathname.to_s 38 | end 39 | 40 | def file? 41 | state.file? 42 | end 43 | 44 | private 45 | 46 | def state 47 | file_system.state(pathname.to_s) 48 | end 49 | end # FakePathname 50 | 51 | class FileSystem 52 | include Adamantium, Anima.new(:file_states) 53 | 54 | def state(filename) 55 | file_states.fetch(filename, FileState::DOES_NOT_EXIST) 56 | end 57 | 58 | def path(filename) 59 | FakePathname.new(file_system: self, pathname: Pathname.new(filename)) 60 | end 61 | end # FileSystem 62 | end # MutantSpec 63 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/timer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Timer do 4 | let(:events) { [] } 5 | let(:object) { described_class.new(process:) } 6 | let(:process) { class_double(Process) } 7 | let(:times) { [0.5, 2.0] } 8 | 9 | before do 10 | allow(process).to receive(:clock_gettime) do |argument| 11 | expect(argument).to be(Process::CLOCK_MONOTONIC) 12 | 13 | events << :clock_gettime 14 | 15 | times.fetch(events.count(:clock_gettime).pred) 16 | end 17 | end 18 | 19 | describe '.now' do 20 | def apply 21 | object.now 22 | end 23 | 24 | it 'returns current monotonic time' do 25 | expect(apply).to be(0.5) 26 | expect(apply).to be(2.0) 27 | end 28 | 29 | it 'calls expected system API' do 30 | expect { apply } 31 | .to change(events, :to_a) 32 | .from([]) 33 | .to(%i[clock_gettime]) 34 | end 35 | end 36 | 37 | describe '#elapsed' do 38 | let(:executions) { [] } 39 | 40 | def apply 41 | object.elapsed do 42 | executions << nil 43 | end 44 | end 45 | 46 | it 'executes the block' do 47 | expect { apply }.to change { executions }.from([]).to([nil]) 48 | end 49 | 50 | it 'returns execution time' do 51 | expect(apply).to eql(1.5) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/status_progressive.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Reporter for progressive output format on scheduler Status objects 8 | class StatusProgressive < self 9 | FORMAT = 'progress: %02d/%02d alive: %d runtime: %0.02fs killtime: %0.02fs mutations/s: %0.02f' 10 | 11 | delegate( 12 | :amount_mutation_results, 13 | :amount_mutations, 14 | :amount_mutations_alive, 15 | :amount_mutations_killed, 16 | :killtime, 17 | :runtime 18 | ) 19 | 20 | # Run printer 21 | # 22 | # @return [undefined] 23 | def run 24 | status( 25 | FORMAT, 26 | amount_mutation_results, 27 | amount_mutations, 28 | amount_mutations_alive, 29 | runtime, 30 | killtime, 31 | mutations_per_second 32 | ) 33 | end 34 | 35 | private 36 | 37 | def object 38 | super.payload 39 | end 40 | 41 | def mutations_per_second 42 | amount_mutation_results / runtime 43 | end 44 | end # StatusProgressive 45 | end # Printer 46 | end # CLI 47 | end # Reporter 48 | end # Mutant 49 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/test/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::Test::Config do 4 | setup_shared_context 5 | 6 | context 'on absent jobs' do 7 | let(:reportable) { config.with(jobs: nil) } 8 | 9 | describe '.call' do 10 | it_reports(<<~'REPORT') 11 | Fail-Fast: false 12 | Integration: null 13 | Jobs: auto 14 | REPORT 15 | end 16 | end 17 | 18 | context 'on present jobs' do 19 | let(:reportable) { config.with(jobs: 10) } 20 | 21 | describe '.call' do 22 | it_reports(<<~'REPORT') 23 | Fail-Fast: false 24 | Integration: null 25 | Jobs: 10 26 | REPORT 27 | end 28 | end 29 | 30 | context 'on absent integration' do 31 | let(:reportable) { config } 32 | 33 | describe '.call' do 34 | it_reports(<<~'REPORT') 35 | Fail-Fast: false 36 | Integration: null 37 | Jobs: auto 38 | REPORT 39 | end 40 | end 41 | 42 | context 'on present integration' do 43 | let(:reportable) { config.with(integration: Mutant::Integration::Config::DEFAULT.with(name: 'foo')) } 44 | 45 | describe '.call' do 46 | it_reports(<<~'REPORT') 47 | Fail-Fast: false 48 | Integration: foo 49 | Jobs: auto 50 | REPORT 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /ruby/test_app/Gemfile.minitest.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | mutant (0.13.5) 5 | diff-lcs (~> 1.3) 6 | irb (~> 1.15.2) 7 | parser (~> 3.3.0) 8 | regexp_parser (~> 2.10) 9 | sorbet-runtime (~> 0.5.0) 10 | unparser (~> 0.8.0) 11 | mutant-minitest (0.13.5) 12 | minitest (~> 5.11) 13 | mutant (= 0.13.5) 14 | mutex_m (~> 0.2) 15 | 16 | GEM 17 | remote: https://rubygems.org/ 18 | specs: 19 | ast (2.4.3) 20 | date (3.4.1) 21 | diff-lcs (1.6.2) 22 | erb (5.0.2) 23 | io-console (0.8.1) 24 | irb (1.15.2) 25 | pp (>= 0.6.0) 26 | rdoc (>= 4.0.0) 27 | reline (>= 0.4.2) 28 | minitest (5.25.5) 29 | mutex_m (0.3.0) 30 | parser (3.3.9.0) 31 | ast (~> 2.4.1) 32 | racc 33 | pp (0.6.2) 34 | prettyprint 35 | prettyprint (0.2.0) 36 | prism (1.5.1) 37 | psych (5.2.6) 38 | date 39 | stringio 40 | racc (1.8.1) 41 | rdoc (6.14.2) 42 | erb 43 | psych (>= 4.0.0) 44 | regexp_parser (2.11.3) 45 | reline (0.6.2) 46 | io-console (~> 0.5) 47 | sorbet-runtime (0.5.12443) 48 | stringio (3.1.7) 49 | unparser (0.8.0) 50 | diff-lcs (~> 1.6) 51 | parser (>= 3.3.0) 52 | prism (>= 1.4) 53 | 54 | PLATFORMS 55 | ruby 56 | x86_64-linux 57 | 58 | DEPENDENCIES 59 | mutant! 60 | mutant-minitest! 61 | 62 | BUNDLED WITH 63 | 2.6.2 64 | -------------------------------------------------------------------------------- /ruby/lib/mutant/test/runner/sink.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Test 5 | module Runner 6 | class Sink 7 | include Anima.new(:env) 8 | 9 | # Initialize object 10 | # 11 | # @return [undefined] 12 | def initialize(*) 13 | super 14 | @start = env.world.timer.now 15 | @test_results = [] 16 | end 17 | 18 | # Runner status 19 | # 20 | # @return [Result::Env] 21 | def status 22 | Result::TestEnv.new( 23 | env:, 24 | runtime: env.world.timer.now - @start, 25 | test_results: @test_results.sort_by!(&:job_index) 26 | ) 27 | end 28 | 29 | # Test if scheduling stopped 30 | # 31 | # @return [Boolean] 32 | def stop? 33 | status.stop? 34 | end 35 | 36 | # Handle mutation finish 37 | # 38 | # @return [self] 39 | def response(response) 40 | if response.error 41 | env.world.stderr.puts(response.log) 42 | fail response.error 43 | end 44 | 45 | @test_results << response.result.with( 46 | job_index: response.job.index, 47 | output: response.log 48 | ) 49 | 50 | self 51 | end 52 | end # Sink 53 | end # Runner 54 | end # Test 55 | end # Mutant 56 | -------------------------------------------------------------------------------- /ruby/meta/range.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mutant::Meta::Example.add :irange do 4 | source '1..100' 5 | 6 | singleton_mutations 7 | mutation '1...100' 8 | mutation '0..100' 9 | mutation '2..100' 10 | mutation 'nil..100' 11 | mutation '1..nil' 12 | mutation '1..0' 13 | mutation '1..1' 14 | mutation '1..99' 15 | mutation '1..101' 16 | end 17 | 18 | Mutant::Meta::Example.add :erange do 19 | source '1...100' 20 | 21 | singleton_mutations 22 | mutation '1..100' 23 | mutation '0...100' 24 | mutation '2...100' 25 | mutation 'nil...100' 26 | mutation '1...nil' 27 | mutation '1...0' 28 | mutation '1...1' 29 | mutation '1...99' 30 | mutation '1...101' 31 | end 32 | 33 | Mutant::Meta::Example.add :erange do 34 | source '1...' 35 | 36 | singleton_mutations 37 | mutation '0...' 38 | mutation '2...' 39 | mutation 'nil...' 40 | end 41 | 42 | Mutant::Meta::Example.add :irange do 43 | source '1..' 44 | 45 | singleton_mutations 46 | mutation '0..' 47 | mutation '2..' 48 | mutation 'nil..' 49 | end 50 | 51 | Mutant::Meta::Example.add :erange do 52 | source '...1' 53 | 54 | singleton_mutations 55 | mutation '...0' 56 | mutation '..1' 57 | mutation '...2' 58 | mutation '...nil' 59 | end 60 | 61 | Mutant::Meta::Example.add :irange do 62 | source '..1' 63 | 64 | singleton_mutations 65 | mutation '..0' 66 | mutation '...1' 67 | mutation '..2' 68 | mutation '..nil' 69 | end 70 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/expression/namespace/exact_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Expression::Namespace::Exact do 4 | let(:object) { parse_expression(input) } 5 | let(:input) { 'TestApp::Literal' } 6 | 7 | describe '#matcher' do 8 | subject { object.matcher(env: instance_double(Mutant::Env)) } 9 | 10 | let(:scope) do 11 | Mutant::Scope.new( 12 | expression: Mutant::Expression::Namespace::Exact.new(scope_name: 'TestApp::Literal'), 13 | raw: TestApp::Literal 14 | ) 15 | end 16 | 17 | context 'when constant does not exist' do 18 | let(:input) { 'TestApp::DoesNotExist' } 19 | 20 | it { should eql(Mutant::Matcher::Null.new) } 21 | end 22 | 23 | context 'when constant exists' do 24 | it { should eql(Mutant::Matcher::Scope.new(scope:)) } 25 | end 26 | end 27 | 28 | describe '#syntax' do 29 | subject { object.syntax } 30 | 31 | it { should eql(input) } 32 | end 33 | 34 | describe '#match_length' do 35 | subject { object.match_length(other) } 36 | 37 | context 'when other is an equivalent expression' do 38 | let(:other) { parse_expression(object.syntax) } 39 | 40 | it { should be(object.syntax.length) } 41 | end 42 | 43 | context 'when other is an unequivalent expression' do 44 | let(:other) { parse_expression('Foo*') } 45 | 46 | it { should be(0) } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ruby/lib/mutant/mutator/node/rescue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Mutator 5 | class Node 6 | # Mutator for rescue nodes 7 | class Rescue < self 8 | 9 | handle :rescue 10 | 11 | children :body 12 | 13 | define_named_child(:else_body, -1) 14 | 15 | RESCUE_INDICES = (1..-2) 16 | 17 | private 18 | 19 | def dispatch 20 | mutate_body 21 | mutate_rescue_bodies 22 | mutate_else_body 23 | end 24 | 25 | def mutate_rescue_bodies 26 | children_indices(RESCUE_INDICES).each do |index| 27 | mutate_child(index) 28 | resbody = AST::Meta::Resbody.new(node: children.fetch(index)) 29 | if resbody.body && !resbody.assignment 30 | emit_concat(resbody.body) 31 | end 32 | end 33 | end 34 | 35 | def emit_concat(child) 36 | if body 37 | emit(s(:begin, body, child)) 38 | else 39 | emit(child) 40 | end 41 | end 42 | 43 | def mutate_body 44 | return unless body 45 | emit_body_mutations 46 | emit(body) 47 | end 48 | 49 | def mutate_else_body 50 | return unless else_body 51 | emit_else_body_mutations 52 | emit_concat(else_body) 53 | end 54 | 55 | end # Rescue 56 | end # Node 57 | end # Mutator 58 | end # Mutant 59 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/block_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Block do 4 | subject { described_class.new(name:, block:) } 5 | 6 | let(:block) { ->(value) { right(value * 2) } } 7 | let(:name) { :external } 8 | 9 | describe '#call' do 10 | def apply 11 | subject.call(input) 12 | end 13 | 14 | let(:input) { 3 } 15 | 16 | context 'when block suceeds' do 17 | it 'returns success' do 18 | expect(apply).to eql(right(6)) 19 | end 20 | end 21 | 22 | context 'when block fails' do 23 | let(:block) { ->(_value) { left('some error') } } 24 | 25 | it 'returns expected error' do 26 | expect(apply).to eql( 27 | left( 28 | Mutant::Transform::Error.new( 29 | cause: nil, 30 | input:, 31 | message: 'some error', 32 | transform: subject 33 | ) 34 | ) 35 | ) 36 | end 37 | end 38 | end 39 | 40 | describe '#slug' do 41 | def apply 42 | subject.slug 43 | end 44 | 45 | it 'returns name' do 46 | expect(apply).to be(name) 47 | end 48 | end 49 | 50 | describe '.capture' do 51 | def apply 52 | described_class.capture(name, &block) 53 | end 54 | 55 | it 'returns expected transform' do 56 | expect(apply).to eql(described_class.new(name:, block:)) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/reporter/cli/printer/status_progressive_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Reporter::CLI::Printer::StatusProgressive do 4 | setup_shared_context 5 | 6 | let(:reportable) { status } 7 | 8 | describe '.call' do 9 | context 'with empty scheduler' do 10 | with(:env_result) { { subject_results: [] } } 11 | 12 | it_reports <<~REPORT 13 | progress: 00/02 alive: 0 runtime: 4.00s killtime: 0.00s mutations/s: 0.00 14 | REPORT 15 | end 16 | 17 | context 'with scheduler active on one subject' do 18 | context 'without progress' do 19 | with(:status) { { active_jobs: [].to_set } } 20 | 21 | it_reports(<<~REPORT) 22 | progress: 02/02 alive: 0 runtime: 4.00s killtime: 2.00s mutations/s: 0.50 23 | REPORT 24 | end 25 | 26 | context 'with progress' do 27 | with(:status) { { active_jobs: [job_b, job_a].to_set } } 28 | 29 | context 'on failure' do 30 | with(:mutation_a_criteria_result) { { test_result: false } } 31 | 32 | it_reports(<<~REPORT) 33 | progress: 02/02 alive: 1 runtime: 4.00s killtime: 2.00s mutations/s: 0.50 34 | REPORT 35 | end 36 | 37 | context 'on success' do 38 | it_reports(<<~REPORT) 39 | progress: 02/02 alive: 0 runtime: 4.00s killtime: 2.00s mutations/s: 0.50 40 | REPORT 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ruby/lib/mutant/reporter/cli/printer/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | class Reporter 5 | class CLI 6 | class Printer 7 | # Env printer 8 | class Env < self 9 | delegate( 10 | :amount_available_tests, 11 | :amount_mutations, 12 | :amount_selected_tests, 13 | :amount_subjects, 14 | :amount_all_tests, 15 | :config, 16 | :test_subject_ratio 17 | ) 18 | 19 | FORMATS = [ 20 | [:info, 'Subjects: %s', :amount_subjects ], 21 | [:info, 'All-Tests: %s', :amount_all_tests ], 22 | [:info, 'Available-Tests: %s', :amount_available_tests], 23 | [:info, 'Selected-Tests: %s', :amount_selected_tests ], 24 | [:info, 'Tests/Subject: %0.2f avg', :test_subject_ratio ], 25 | [:info, 'Mutations: %s', :amount_mutations ] 26 | ].each(&:freeze) 27 | 28 | # Run printer 29 | # 30 | # @return [undefined] 31 | def run 32 | info('Mutant environment:') 33 | visit(Config, config) 34 | FORMATS.each do |report, format, value| 35 | __send__(report, format, __send__(value)) 36 | end 37 | end 38 | end # Env 39 | end # Printer 40 | end # CLI 41 | end # Reporter 42 | end # Mutant 43 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/parallel/source/array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Parallel::Source::Array do 4 | subject { described_class.new(jobs:) } 5 | 6 | let(:jobs) { %i[a b c] } 7 | 8 | describe '#next' do 9 | def job(**attributes) 10 | Mutant::Parallel::Source::Job.new(attributes) 11 | end 12 | 13 | def apply 14 | subject.next 15 | end 16 | 17 | context 'when there is a next job' do 18 | it 'returns that job' do 19 | expect(apply).to eql(job(index: 0, payload: :a)) 20 | end 21 | 22 | it 'does not return the same job twice' do 23 | expect(apply).to eql(job(index: 0, payload: :a)) 24 | expect(apply).to eql(job(index: 1, payload: :b)) 25 | expect(apply).to eql(job(index: 2, payload: :c)) 26 | end 27 | end 28 | 29 | context 'when there is no next job' do 30 | let(:jobs) { [] } 31 | 32 | it 'raises error' do 33 | expect { apply }.to raise_error(Mutant::Parallel::Source::NoJobError) 34 | end 35 | end 36 | end 37 | 38 | describe '#next?' do 39 | def apply 40 | subject.next? 41 | end 42 | 43 | context 'when there is a next job' do 44 | it 'returns true' do 45 | expect(apply).to be(true) 46 | end 47 | end 48 | 49 | context 'when there is no next job' do 50 | let(:jobs) { [] } 51 | 52 | it 'returns false' do 53 | expect(apply).to be(false) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /ruby/mutant.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('lib/mutant/version', __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = 'mutant' 7 | gem.version = Mutant::VERSION.dup 8 | gem.authors = ['Markus Schirp'] 9 | gem.email = ['mbj@schirp-dso.com'] 10 | gem.description = 'Mutation Testing for Ruby.' 11 | gem.summary = '' 12 | gem.homepage = 'https://github.com/mbj/mutant' 13 | gem.license = 'Nonstandard' 14 | 15 | gem.require_paths = %w[lib] 16 | 17 | exclusion = Dir.glob('lib/mutant/{integration/{minitest,rspec}.rb,minitest/**.rb}') 18 | 19 | gem.files = Dir.glob('lib/**/*') - exclusion 20 | gem.extra_rdoc_files = %w[LICENSE] 21 | gem.executables = %w[mutant mutant-ruby] 22 | 23 | gem.metadata['rubygems_mfa_required'] = 'true' 24 | gem.metadata['source_code_uri'] = 'https://github.com/mbj/mutant' 25 | 26 | gem.required_ruby_version = '>= 3.2' 27 | 28 | gem.add_dependency('diff-lcs', '~> 1.3') 29 | gem.add_dependency('irb', '~> 1.15.2') 30 | gem.add_dependency('parser', '~> 3.3.0') 31 | gem.add_dependency('regexp_parser', '~> 2.10') 32 | gem.add_dependency('sorbet-runtime', '~> 0.5.0') 33 | gem.add_dependency('unparser', '~> 0.8.0') 34 | 35 | gem.add_development_dependency('rspec', '~> 3.10') 36 | gem.add_development_dependency('rspec-core', '~> 3.10') 37 | gem.add_development_dependency('rspec-its', '~> 1.3.0') 38 | gem.add_development_dependency('rubocop', '~> 1.7') 39 | end 40 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Parser do 4 | let(:object) { described_class.new } 5 | 6 | describe '#call' do 7 | let(:path) { instance_double(Pathname, expand_path: expanded_path) } 8 | 9 | let(:expanded_path) { instance_double(Pathname) } 10 | 11 | let(:expected_node) { s(:sym, :source) } 12 | 13 | subject { object.call(path) } 14 | 15 | let(:buffer) do 16 | Unparser.buffer(<<~'RUBY') 17 | :source # comment_a 18 | :source # comment_b 19 | RUBY 20 | end 21 | 22 | let(:expected_associations) do 23 | associations = {} 24 | associations.compare_by_identity 25 | 26 | associations[s(:sym, :source)] = [ 27 | Parser::Source::Comment.new(Parser::Source::Range.new(buffer, 8, 19)) 28 | ] 29 | 30 | associations[s(:sym, :source)] = [ 31 | Parser::Source::Comment.new(Parser::Source::Range.new(buffer, 28, 39)) 32 | ] 33 | 34 | associations 35 | end 36 | 37 | before do 38 | allow(path).to receive(:read).and_return(buffer.source) 39 | end 40 | 41 | it 'returns parsed source' do 42 | expect(subject.inspect).to eql( 43 | Mutant::AST.new( 44 | comment_associations: expected_associations, 45 | node: s(:begin, expected_node, expected_node) 46 | ).inspect 47 | ) 48 | end 49 | 50 | it 'is idempotent' do 51 | source = object.call(path) 52 | expect(subject).to be(source) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /ruby/spec/integration/mutant/isolation/fork_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Isolation::Fork, mutant: false do 4 | def apply(&block) 5 | Mutant::Config::DEFAULT.isolation.call(timeout, &block) 6 | end 7 | 8 | let(:timeout) { nil } 9 | 10 | it 'isolates local writes' do 11 | a = 1 12 | 13 | expect { apply { a = 2 } }.to_not(change { a }.from(1)) 14 | end 15 | 16 | it 'captures console output' do 17 | result = apply do 18 | $stdout.puts('foo') 19 | $stderr.puts('bar') 20 | end 21 | 22 | expect(result.log).to eql("foo\nbar\n") 23 | end 24 | 25 | it 'allows to read result' do 26 | result = apply { :foo } 27 | 28 | expect(result.value).to eql(:foo) 29 | end 30 | 31 | context 'with configured timeout' do 32 | let(:timeout) { 1.0 } 33 | 34 | context 'when block exits within timeout' do 35 | def apply 36 | super do 37 | :value 38 | end 39 | end 40 | 41 | it 'returns successful result' do 42 | result = apply 43 | expect(result.timeout).to be(nil) 44 | expect(result.value).to be(:value) 45 | end 46 | end 47 | 48 | context 'when block does not exit within timeout' do 49 | def apply 50 | super do 51 | sleep 10 52 | :value 53 | end 54 | end 55 | 56 | it 'returns successful result' do 57 | result = apply 58 | expect(result.timeout).to be(1.0) 59 | expect(result.value).to be(nil) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/config/coverage_criteria_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Config::CoverageCriteria do 4 | describe '#merge' do 5 | let(:original) do 6 | described_class.new( 7 | test_result:, 8 | timeout:, 9 | process_abort: 10 | ) 11 | end 12 | 13 | let(:test_result) { nil } 14 | let(:timeout) { nil } 15 | let(:process_abort) { nil } 16 | 17 | def apply 18 | original.merge(other) 19 | end 20 | 21 | %i[test_result timeout process_abort].each do |key| 22 | context "for #{key} attributze" do 23 | context 'when original is not nil' do 24 | let(key) { true } 25 | 26 | context 'and other is nil' do 27 | let(:other) { original.with(key => nil) } 28 | 29 | it 'returns original value' do 30 | expect(apply.public_send(key)).to be(true) 31 | end 32 | end 33 | 34 | context 'and other is not nil' do 35 | let(:other) { original.with(key => false) } 36 | 37 | it 'returns original value' do 38 | expect(apply.public_send(key)).to be(false) 39 | end 40 | end 41 | end 42 | 43 | context 'when original is nil' do 44 | let(key) { nil } 45 | 46 | let(:other) { original.with(key => false) } 47 | 48 | it 'returns other value' do 49 | expect(apply.public_send(key)).to be(false) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/transform/bool_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Transform::Boolean do 4 | subject { described_class.new } 5 | 6 | describe '#call' do 7 | def apply 8 | subject.call(input) 9 | end 10 | 11 | context 'on true' do 12 | let(:input) { true } 13 | 14 | it 'returns sucess' do 15 | expect(apply).to eql(Mutant::Either::Right.new(input)) 16 | end 17 | end 18 | 19 | context 'on false' do 20 | let(:input) { false } 21 | 22 | it 'returns sucess' do 23 | expect(apply).to eql(Mutant::Either::Right.new(input)) 24 | end 25 | end 26 | 27 | context 'on nil input' do 28 | let(:input) { nil } 29 | 30 | let(:error) do 31 | Mutant::Transform::Error.new( 32 | cause: nil, 33 | input:, 34 | message: 'Expected: boolean but got: nil', 35 | transform: subject 36 | ) 37 | end 38 | 39 | it 'returns failure' do 40 | expect(apply).to eql(Mutant::Either::Left.new(error)) 41 | end 42 | end 43 | 44 | context 'on truthy input' do 45 | let(:input) { '' } 46 | 47 | let(:error) do 48 | Mutant::Transform::Error.new( 49 | cause: nil, 50 | input:, 51 | message: 'Expected: boolean but got: ""', 52 | transform: subject 53 | ) 54 | end 55 | 56 | it 'returns failure' do 57 | expect(apply).to eql(Mutant::Either::Left.new(error)) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /ruby/lib/mutant/expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mutant 4 | 5 | # Abstract base class for match expression 6 | class Expression 7 | fragment = /[A-Za-z][A-Za-z\d_]*/ 8 | SCOPE_NAME_PATTERN = /(?#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/ 9 | SCOPE_SYMBOL_PATTERN = '(?[.#])' 10 | 11 | private_constant(*constants(false)) 12 | 13 | def self.new(*) 14 | super.freeze 15 | end 16 | 17 | # Match length with other expression 18 | # 19 | # @param [Expression] other 20 | # 21 | # @return [Integer] 22 | def match_length(other) 23 | if syntax.eql?(other.syntax) 24 | syntax.length 25 | else 26 | 0 27 | end 28 | end 29 | 30 | # Test if expression is prefix 31 | # 32 | # @param [Expression] other 33 | # 34 | # @return [Boolean] 35 | def prefix?(other) 36 | !match_length(other).zero? 37 | end 38 | 39 | # Try to parse input into expression of receiver class 40 | # 41 | # @param [String] input 42 | # 43 | # @return [Expression] 44 | # when successful 45 | # 46 | # @return [nil] 47 | # otherwise 48 | def self.try_parse(input) 49 | match = self::REGEXP.match(input) 50 | from_match(match) if match 51 | end 52 | 53 | def self.from_match(match) 54 | names = anima.attribute_names 55 | new(names.zip(names.map(&match.public_method(:[]))).to_h) 56 | end 57 | private_class_method :from_match 58 | 59 | end # Expression 60 | end # Mutant 61 | -------------------------------------------------------------------------------- /ruby/spec/unit/mutant/mutation/generation_error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mutant::Mutation::GenerationError do 4 | subject do 5 | described_class.new( 6 | node: s(:false), 7 | subject: mutant_subject, 8 | unparser_validation: 9 | ) 10 | end 11 | 12 | let(:mutant_subject) do 13 | instance_double( 14 | Mutant::Subject, 15 | identification: '', 16 | node: s(:true), 17 | source: 'true' 18 | ) 19 | end 20 | 21 | let(:unparser_validation) do 22 | instance_double( 23 | Unparser::Validation, 24 | original_source: right('false'), 25 | report: '' 26 | ) 27 | end 28 | 29 | describe '#report' do 30 | def apply 31 | subject.report 32 | end 33 | 34 | it 'returns expected value' do 35 | expect(apply).to eql(<<~MESSAGE) 36 | === Mutation-Generation-Error === 37 | This is a mutant internal issue detected by a mutant internal cross check. 38 | Please report an issue with the details below. 39 | 40 | Subject: . 41 | 42 | Mutation-Source-Diff: 43 | @@ -1 +1 @@ 44 | \e[31m-true 45 | \e[0m\e[32m+false 46 | \e[0m 47 | 48 | Mutation-Node-Diff: 49 | @@ -1 +1 @@ 50 | \e[31m-(true) 51 | \e[0m\e[32m+(false) 52 | \e[0m 53 | 54 | Unparser-Validation: 55 | 56 | MESSAGE 57 | end 58 | end 59 | end 60 | --------------------------------------------------------------------------------