Skip to content

Commit

Permalink
Add matcher config in configuration file
Browse files Browse the repository at this point in the history
  • Loading branch information
mbj committed Jan 1, 2021
1 parent 5a7c8f2 commit b8b207f
Show file tree
Hide file tree
Showing 17 changed files with 186 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: ./mutant.sh --since HEAD~1 -- 'Mutant*'
- run: bundle exec mutant run --zombie --since HEAD~1
ruby-integration-misc:
name: Integration Misc
runs-on: ${{ matrix.os }}
Expand Down
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Unreleased

* [#1176](https://github.com/mbj/mutant/pull/1176)

Allow [subject matcher configuration](https://github.com/mbj/mutant/tree/master/doc/configuration#matcher)
in the configuration file.

* Reintroduce regexp mutation support [#1166](https://github.com/mbj/mutant/pull/1166)

# v0.10.23 2020-12-30
Expand Down
36 changes: 36 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,42 @@ When `fail_fast` is enabled, mutant will stop as soon as it encounters an alive
fail_fast: true
```

#### `matcher`

Allows to set subject matchers in the configration file.

```yaml:
matcher:
# Subject expressions to find subjects for mutation testing.
# Multiple entries are allowed and matches from each expression
# are unioned.
#
# Subject expressions can also be specified on the command line. Example:
# `bundle exec mutant run YourSubject`
#
# Note that expressions from the command line replace the subjects
# configured # in the config file!
subjects:
- Your::App::Namespace # select all subjects on a specific constant
- Your::App::Namespace* # select all subjects on a specific constant, recursively
- Your::App::Namespace#some_method # select a specific instance method
- Your::App::Namespace.some_method # select a specific class method
# Expressions of subjects to ignore during mutation testing.
# Multiple entries are allowed and matches from each expression
# are unioned.
#
# Subject ignores can also be specified on the command line, via `--ignore-subject`. Example:
# `bundle exec mutant run --ignore-subject YourSubject#some_method`
#
# Note that subject ignores from the command line are added to the subject ignores
# configured on the command line!
ignore:
- Your::App::Namespace::Dirty # ignore all subjects on a specific constant
- Your::App::Namespace::Dirty* # ignore all subjects on a specific constant, recursively
- Your::App::Namespace::Dirty#some_method # ignore a specific instance method
- Your::App::Namespace::Dirty#some_method # ignore a specific class method
```
#### `jobs`

Specify how many processes mutant uses to kill mutations. Defaults to the number of processors on your system.
Expand Down
2 changes: 1 addition & 1 deletion docs/mutant-minitest.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ This prints a report like:

```sh
Mutant environment:
Matcher: #<Mutant::Matcher::Config match_expressions: [AUOM*]>
Matcher: #<Mutant::Matcher::Config subjects: [AUOM*]>
Integration: Mutant::Integration::Minitest
Jobs: 8
Includes: ["lib"]
Expand Down
4 changes: 2 additions & 2 deletions docs/mutant-rspec.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This prints a report like:

```sh
Mutant environment:
Matcher: #<Mutant::Matcher::Config match_expressions: [AUOM*]>
Matcher: #<Mutant::Matcher::Config subjects: [AUOM*]>
Integration: Mutant::Integration::Rspec
Jobs: 8
Includes: ["lib"]
Expand Down Expand Up @@ -89,7 +89,7 @@ evil:AUOM::Unit.new:/home/mrh-dev/example/auom/lib/auom/unit.rb:172:45e17
end
-----------------------
Mutant configuration:
Matcher: #<Mutant::Matcher::Config match_expressions: [AUOM*]>
Matcher: #<Mutant::Matcher::Config subjects: [AUOM*]>
Integration: Mutant::Integration::Rspec
Jobs: 8
Includes: ["lib"]
Expand Down
3 changes: 2 additions & 1 deletion lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module Mutant
SCOPE_OPERATOR = '::'
end # Mutant

require 'mutant/transform'
require 'mutant/bootstrap'
require 'mutant/version'
require 'mutant/env'
Expand Down Expand Up @@ -176,14 +177,14 @@ module Mutant
require 'mutant/expression/namespace'
require 'mutant/test'
require 'mutant/timer'
require 'mutant/transform'
require 'mutant/integration'
require 'mutant/integration/null'
require 'mutant/selector'
require 'mutant/selector/expression'
require 'mutant/selector/null'
require 'mutant/world'
require 'mutant/config'
require 'mutant/config/coverage_criteria'
require 'mutant/cli'
require 'mutant/cli/command'
require 'mutant/cli/command/subscription'
Expand Down
6 changes: 3 additions & 3 deletions lib/mutant/cli/command/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def expand(file_config)

def parse_remaining_arguments(arguments)
Mutant.traverse(@config.expression_parser, arguments)
.fmap do |match_expressions|
matcher(match_expressions: match_expressions)
.fmap do |expressions|
matcher(subjects: expressions)
self
end
end
Expand Down Expand Up @@ -82,7 +82,7 @@ def add_matcher_options(parser)
parser.separator('Matcher:')

parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
add_matcher(:ignore_expressions, @config.expression_parser.call(pattern).from_right)
add_matcher(:ignore, @config.expression_parser.call(pattern).from_right)
end
parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
add_matcher(:start_expressions, @config.expression_parser.call(pattern).from_right)
Expand Down
63 changes: 10 additions & 53 deletions lib/mutant/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,54 +37,6 @@ class Config

private_constant(*constants(false))

class CoverageCriteria
include Anima.new(:process_abort, :test_result, :timeout)

EMPTY = new(
process_abort: nil,
test_result: nil,
timeout: nil
)

DEFAULT = new(
process_abort: false,
test_result: true,
timeout: false
)

TRANSFORM =
Transform::Sequence.new(
[
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
],
required: []
),
Transform::Hash::Symbolize.new,
->(value) { Either::Right.new(DEFAULT.with(**value)) }
]
)

def merge(other)
self.class.new(
process_abort: overwrite(other, :process_abort),
test_result: overwrite(other, :test_result),
timeout: overwrite(other, :timeout)
)
end

private

def overwrite(other, attribute_name)
other_value = other.public_send(attribute_name)

other_value.nil? ? public_send(attribute_name) : other_value
end
end # CoverageCriteria

# Merge with other config
#
# @param [Config] other
Expand Down Expand Up @@ -116,18 +68,22 @@ def merge(other)
#
# @return [Either<String,Config>]
def self.load_config_file(world)
config = DEFAULT
files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)

if files.one?
load_contents(files.first).fmap(&config.public_method(:with))
load_contents(files.first).fmap(&method(:from_attributes))
elsif files.empty?
Either::Right.new(config)
Either::Right.new(DEFAULT)
else
Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
end
end

def self.from_attributes(attributes)
DEFAULT.with(attributes)
end
private_class_method :from_attributes

# Expand config with defaults
#
# @return [Config]
Expand Down Expand Up @@ -159,13 +115,14 @@ def self.env
Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
Transform::Hash::Key.new('integration', Transform::STRING),
Transform::Hash::Key.new('jobs', Transform::INTEGER),
Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY),
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER)
],
required: []
),
Expand Down
54 changes: 54 additions & 0 deletions lib/mutant/config/coverage_criteria.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module Mutant
class Config
# Configuration of coverge conditions
class CoverageCriteria
include Anima.new(:process_abort, :test_result, :timeout)

EMPTY = new(
process_abort: nil,
test_result: nil,
timeout: nil
)

DEFAULT = new(
process_abort: false,
test_result: true,
timeout: false
)

TRANSFORM =
Transform::Sequence.new(
[
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
],
required: []
),
Transform::Hash::Symbolize.new,
->(value) { Either::Right.new(DEFAULT.with(**value)) }
]
)

def merge(other)
self.class.new(
process_abort: overwrite(other, :process_abort),
test_result: overwrite(other, :test_result),
timeout: overwrite(other, :timeout)
)
end

private

def overwrite(other, attribute_name)
other_value = other.public_send(attribute_name)

other_value.nil? ? public_send(attribute_name) : other_value
end
end # CoverageCriteria
end # Config
end # Mutant
4 changes: 2 additions & 2 deletions lib/mutant/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Matcher
# @return [Matcher]
def self.from_config(config)
Filter.new(
Chain.new(config.match_expressions.map(&:matcher)),
Chain.new(config.subjects.map(&:matcher)),
method(:allowed_subject?).curry.call(config)
)
end
Expand All @@ -42,7 +42,7 @@ def self.select_subject?(config, subject)
#
# @return [Boolean]
def self.ignore_subject?(config, subject)
config.ignore_expressions.any? do |expression|
config.ignore.any? do |expression|
expression.prefix?(subject.expression)
end
end
Expand Down
31 changes: 25 additions & 6 deletions lib/mutant/matcher/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ class Matcher
# Subject matcher configuration
class Config
include Adamantium, Anima.new(
:ignore_expressions,
:match_expressions,
:ignore,
:subjects,
:start_expressions,
:subject_filters
)
Expand All @@ -17,15 +17,34 @@ class Config
ENUM_DELIMITER = ','
EMPTY_ATTRIBUTES = 'empty'
PRESENTATIONS = IceNine.deep_freeze(
ignore_expressions: :syntax,
match_expressions: :syntax,
start_expressions: :syntax,
subject_filters: :inspect
ignore: :syntax,
start_expressions: :syntax,
subject_filters: :inspect,
subjects: :syntax
)
private_constant(*constants(false))

DEFAULT = new(Hash[anima.attribute_names.map { |name| [name, []] }])

expression = ->(input) { Mutant::Config::DEFAULT.expression_parser.call(input) }

expression_array = Transform::Array.new(expression)

LOADER =
Transform::Sequence.new(
[
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('subjects', expression_array),
Transform::Hash::Key.new('ignore', expression_array)
],
required: []
),
Transform::Hash::Symbolize.new,
->(attributes) { Either::Right.new(DEFAULT.with(attributes)) }
]
)

# Inspection string
#
# @return [String]
Expand Down
10 changes: 10 additions & 0 deletions mutant.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,13 @@ requires:
- mutant
- mutant/integration/rspec
- mutant/meta
matcher:
subjects:
- Mutant*
ignore:
- Mutant::Isolation::Fork::Parent#call
- Mutant::Mutator::Node::Argument#skip?
- Mutant::Mutator::Node::Literal::Regex#body
- Mutant::Mutator::Node::ProcargZero#dispatch
- Mutant::Mutator::Node::When#mutate_conditions
- Mutant::Zombifier#call
2 changes: 1 addition & 1 deletion scripts/devloop.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
while inotifywait **/*.rb Gemfile Gemfile.shared mutant.gemspec; do
bundle exec rspec spec/unit -fd --fail-fast --order default \
&& bundle exec ./mutant.sh --since master --fail-fast -- 'Mutant*' \
&& bundle exec mutant run --since master --fail-fast --zombie -- 'Mutant*' \
&& bundle exec rubocop
done
Loading

0 comments on commit b8b207f

Please sign in to comment.