Skip to content

Commit

Permalink
Add per subject inline disable configuration
Browse files Browse the repository at this point in the history
* This allows to place `mutant:disable` comments next to subjects to
  mark them as disabled.
* Inheritance is not supported currently, so a comment on a class will
  not make this entire class disabled.
  • Loading branch information
mbj committed Apr 25, 2022
1 parent 697d439 commit d52ff56
Show file tree
Hide file tree
Showing 21 changed files with 270 additions and 11 deletions.
16 changes: 16 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# v0.11.8 2022-04-25

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

Add inline mutant disable configuration. This allows individual subjects to be marked as
disbled directly in the code.

Use:

` ```
class Something
# mutant:disable
def some_method
end
end

# v0.11.7 2022-04-24

* [#1319](https://github.com/mbj/mutant/pull/1319)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
mutant (0.11.7)
mutant (0.11.8)
diff-lcs (~> 1.3)
parser (~> 3.1.0)
regexp_parser (~> 2.3, >= 2.3.1)
Expand Down
28 changes: 28 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Configuration

There are 3 ways of configuring mutant:

1. In the source code via processing comments.
2. Via the CLI
3. Via a config file.

### Processing Comments

Mutant currently only supports the `mutant:disable` directive that can be added in a
source code comment to ignore a specific subject.

Example:

```ruby
class SomeClass
# mutant:disable
def some_method
end
end
```

More inline configuration will be made available over time.

### Configuration File

Mutant can be configured with a config file that can be named one of the following: `.mutant.yml`, `config/mutant.yml`, or `mutant.yml`

The following options can be configured through the config file:
Expand Down Expand Up @@ -92,6 +117,9 @@ matcher:
#
# Note that subject ignores from the command line are added to the subject ignores
# configured on the command line!
#
# Also matcher ignores generally shold be used for entire namespaces, and individual
# methods be disabled directly in source code via `mutant:disable` directives.
ignore:
- Your::App::Namespace::Dirty # ignore all subjects on a specific constant
- Your::App::Namespace::Dirty* # ignore all subjects on a specific constant, recursively
Expand Down
1 change: 1 addition & 0 deletions lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ module Mutant
require 'mutant/context'
require 'mutant/scope'
require 'mutant/subject'
require 'mutant/subject/config'
require 'mutant/subject/method'
require 'mutant/subject/method/instance'
require 'mutant/subject/method/singleton'
Expand Down
2 changes: 1 addition & 1 deletion lib/mutant/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def self.from_config(config)
end

def self.allowed_subject?(config, subject)
select_subject?(config, subject) && !ignore_subject?(config, subject)
select_subject?(config, subject) && !ignore_subject?(config, subject) && !subject.inline_disabled?
end
private_class_method :allowed_subject?

Expand Down
1 change: 1 addition & 0 deletions lib/mutant/matcher/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def subject
node = matched_node_path.last || return

self.class::SUBJECT_CLASS.new(
config: Subject::Config.parse(ast.comment_associations.fetch(node, [])),
context: context,
node: node,
visibility: visibility
Expand Down
6 changes: 5 additions & 1 deletion lib/mutant/subject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Mutant
# Subject of a mutation
class Subject
include AbstractType, Adamantium, Enumerable
include Anima.new(:context, :node)
include Anima.new(:config, :context, :node)

# Mutations for this subject
#
Expand All @@ -19,6 +19,10 @@ def mutations
end
memoize :mutations

def inline_disabled?
config.inline_disable
end

# Source path
#
# @return [Pathname]
Expand Down
25 changes: 25 additions & 0 deletions lib/mutant/subject/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Mutant
class Subject
class Config
include Adamantium, Anima.new(:inline_disable)

DEFAULT = new(inline_disable: false)

DISABLE_REGEXP = /(\s|^)mutant:disable(?:\s|$)/.freeze
SYNTAX_REGEXP = /\A(?:#|=begin\n)/.freeze

def self.parse(comments)
new(
inline_disable: comments.any? { |comment| DISABLE_REGEXP.match?(comment_body(comment)) }
)
end

def self.comment_body(comment)
comment.text.sub(SYNTAX_REGEXP, '')
end
private_class_method :comment_body
end # Config
end # Subject
end # Mutant
2 changes: 1 addition & 1 deletion lib/mutant/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Mutant
# Current mutant version
VERSION = '0.11.7'
VERSION = '0.11.8'
end # Mutant
6 changes: 2 additions & 4 deletions spec/unit/mutant/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -496,10 +496,8 @@ def self.main_body

let(:subject_a) do
Mutant::Subject::Method::Instance.new(
context: Mutant::Context.new(
Object,
'subject.rb'
),
config: Mutant::Subject::Config::DEFAULT,
context: Mutant::Context.new(Object, 'subject.rb'),
node: s(:def, :send, s(:args), nil),
visibility: :public
)
Expand Down
1 change: 1 addition & 0 deletions spec/unit/mutant/matcher/descendants_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def apply
let(:expected_subjects) do
[
Mutant::Subject::Method::Instance.new(
config: Mutant::Subject::Config::DEFAULT,
context: Mutant::Context.new(
TestApp::Foo::Bar::Baz,
TestApp::ROOT.join('lib/test_app.rb')
Expand Down
13 changes: 13 additions & 0 deletions spec/unit/mutant/matcher/method/instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def arguments
let(:expected_subjects) do
[
Mutant::Subject::Method::Instance.new(
config: Mutant::Subject::Config::DEFAULT,
context: context,
node: s(:def, :bar, s(:args), nil),
visibility: expected_visibility
Expand Down Expand Up @@ -213,4 +214,16 @@ def arguments

it_should_behave_like 'a method matcher'
end

context 'on inline disabled method' do
let(:scope) { TestApp::InlineDisabled }
let(:method_line) { 148 }
let(:method_arity) { 0 }

it_should_behave_like 'a method matcher' do
it 'returns disabled inline config' do
expect(mutation_subject.config.inline_disable).to be(true)
end
end
end
end
12 changes: 12 additions & 0 deletions spec/unit/mutant/matcher/method/singleton_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,16 @@ def arguments

it_should_behave_like 'a method matcher'
end

context 'on inline disabled method' do
let(:scope) { TestApp::InlineDisabled }
let(:method_line) { 152 }
let(:method_arity) { 0 }

it_should_behave_like 'a method matcher' do
it 'returns disabled inline config' do
expect(mutation_subject.config.inline_disable).to be(true)
end
end
end
end
26 changes: 23 additions & 3 deletions spec/unit/mutant/matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,43 @@ def apply
end

let(:subject_a) do
instance_double(Mutant::Subject, 'subject a', expression: expression('Foo::Bar#a'))
instance_double(
Mutant::Subject,
'subject a',
expression: expression('Foo::Bar#a'),
inline_disabled?: false
)
end

let(:subject_b) do
instance_double(Mutant::Subject, 'subject b', expression: expression('Foo::Bar#b'))
instance_double(
Mutant::Subject,
'subject b',
expression: expression('Foo::Bar#b'),
inline_disabled?: false
)
end

def expression(input, matcher = anon_matcher)
expression_class.new(parse_expression(input), matcher)
end

context 'empty ignores and empty filter' do
context 'no restrictions of any kinds' do
it 'returns expected subjects' do
expect(apply.call(env)).to eql([subject_a, subject_b])
end
end

context 'with explicit disable' do
before do
allow(subject_b).to receive_messages(inline_disabled?: true)
end

it 'returns expected subjects' do
expect(apply.call(env)).to eql([subject_a])
end
end

context 'with ignore matching a subject' do
let(:ignore_expressions) { [subject_b.expression] }

Expand Down
1 change: 1 addition & 0 deletions spec/unit/mutant/selector/expression_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

let(:mutation_subject) do
subject_class.new(
config: Mutant::Subject::Config::DEFAULT,
context: context,
node: node
)
Expand Down
108 changes: 108 additions & 0 deletions spec/unit/mutant/subject/config_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# frozen_string_literal: true

RSpec.describe Mutant::Subject::Config do
describe '.parse' do
def apply
described_class.parse(comments)
end

let(:comments) do
node, comments = Unparser.parse_with_comments(source)

::Parser::Source::Comment.associate_by_identity(node, comments).fetch(node, [])
end

shared_examples 'returns default config' do
it 'returns default config' do
expect(apply).to eql(described_class::DEFAULT)
end
end

shared_examples 'returns disabled config' do
it 'returns default config' do
expect(apply).to eql(described_class.new(inline_disable: true))
end
end

context 'on empty comments' do
let(:source) do
<<~'RUBY'
def foo
end
RUBY
end

include_examples 'returns default config'
end

context 'on comment not mentioning a mutant disable' do
context 'in a line comment' do
let(:source) do
<<~'RUBY'
# rubocop:disable Metrics/Something
def foo
end
RUBY
end

include_examples 'returns default config'
end

context 'in a block comment' do
let(:source) do
<<~'RUBY'
=begin
rubocop:disable Metrics/Something
=end
def foo
end
RUBY
end

include_examples 'returns default config'
end
end

context 'on comment mentioning a mutant disable' do
context 'in a block comment' do
let(:source) do
<<~'RUBY'
=begin
mutant:disable
=end
def foo
end
RUBY
end

include_examples 'returns disabled config'
end

context 'in a line comment' do
context 'with space' do
let(:source) do
<<~'RUBY'
# mutant:disable
def foo
end
RUBY
end

include_examples 'returns disabled config'
end

context 'without space' do
let(:source) do
<<~'RUBY'
#mutant:disable
def foo
end
RUBY
end

include_examples 'returns disabled config'
end
end
end
end
end
2 changes: 2 additions & 0 deletions spec/unit/mutant/subject/method/instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
RSpec.describe Mutant::Subject::Method::Instance do
let(:object) do
described_class.new(
config: Mutant::Subject::Config::DEFAULT,
context: context,
node: node,
visibility: :private
Expand Down Expand Up @@ -86,6 +87,7 @@ def self.name
RSpec.describe Mutant::Subject::Method::Instance::Memoized do
let(:object) do
described_class.new(
config: Mutant::Subject::Config::DEFAULT,
context: context,
node: node,
visibility: :public
Expand Down
1 change: 1 addition & 0 deletions spec/unit/mutant/subject/method/metaclass_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
RSpec.describe Mutant::Subject::Method::Metaclass do
let(:object) do
described_class.new(
config: Mutant::Subject::Config::DEFAULT,
context: context,
node: node,
visibility: :public
Expand Down
Loading

0 comments on commit d52ff56

Please sign in to comment.