Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add matcher config in configuration file #1176

Merged
merged 3 commits into from
Jan 1, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
dgollahon marked this conversation as resolved.
Show resolved Hide resolved
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/docs/configuration.md#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*]>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good change I think--it reads more clearly.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is much nicer to read. Its clear the domain of this method is matching, else the class would not be named Mutant::Matcher.

So the match_ prefix was redundant always.

Just naming the key expressions would IMO also suck as it than would have a sibling ignore_expressions which does not clearly indicate what the difference between both would be.

Hence replacing this with subjects makes it very clear. And subject_expressions is kind of redundant as we can only match expressions.

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'
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact this has to move up the require chain to toplevel, indicates it likely belongs to a separate library.

Once I make nicer "input error location rendering" this is worthy of a separate lib.

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
62 changes: 8 additions & 54 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,13 +68,14 @@ 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?)
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(&DEFAULT.public_method(:with))
elsif files.empty?
Either::Right.new(config)
Either::Right.new(DEFAULT)
else
Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
end
Expand Down Expand Up @@ -159,13 +112,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) }),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I "really" like this require sequence:

require 'foo'
require 'foo/bar'

But we have to access the constant from foo/bar here, hence as we are in class scope and the require for `foo/bar' did not happen yet we have to use a lambda indirection.

The alternative would be:

require 'foo/bar'
require 'foo'

which sucks. For the moment I eat it. Would love of Ruby had a module system ;).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love of Ruby had a module system ;)

yeahh... I feel that one!

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)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, paired with adding the constant Mutant::Config::LOADER is the only real change to make config file support for matchers.

],
required: []
),
Expand Down
61 changes: 61 additions & 0 deletions lib/mutant/config/coverage_criteria.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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)) }
]
)

# Merge coverage criteria with other instance
#
# Values from the other instance have precedence.
#
# @param [CoverageCriteria] other
#
# @return [CoverageCriteria]
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
13 changes: 0 additions & 13 deletions mutant.sh

This file was deleted.

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