diff --git a/docs/configuration.md b/docs/configuration.md index 0a44f82f7..0a0a1371f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,6 +126,7 @@ matcher: - Your::App::Namespace#some_method # select a specific instance method - Your::App::Namespace.some_method # select a specific class method - descendants:ApplicationController # select all descendands of application controller (and itself) + - source:lib/**/*.rb # select all subjects that are defined in toplevel constants (modules and classes), recursively # Expressions of subjects to ignore during mutation testing. # Multiple entries are allowed and matches from each expression # are unioned. diff --git a/lib/mutant.rb b/lib/mutant.rb index fa91f9ab6..ec967bd00 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -204,6 +204,7 @@ module Mutant require 'mutant/expression/methods' require 'mutant/expression/namespace' require 'mutant/expression/parser' + require 'mutant/expression/source' require 'mutant/test' require 'mutant/test/runner' require 'mutant/test/runner/sink' @@ -347,7 +348,8 @@ class Config Expression::Method, Expression::Methods, Expression::Namespace::Exact, - Expression::Namespace::Recursive + Expression::Namespace::Recursive, + Expression::Source ] ), environment_variables: EMPTY_HASH, diff --git a/lib/mutant/bootstrap.rb b/lib/mutant/bootstrap.rb index 1167f7d36..d2b058295 100644 --- a/lib/mutant/bootstrap.rb +++ b/lib/mutant/bootstrap.rb @@ -38,7 +38,7 @@ def self.call(env) .with(matchable_scopes: matchable_scopes(env)) matched_subjects = env.record(:subject_match) do - Matcher.from_config(env.config.matcher).call(env) + Matcher.expand(env: env).call(env) end selected_subjects = subject_select(env, matched_subjects) diff --git a/lib/mutant/expression/descendants.rb b/lib/mutant/expression/descendants.rb index 5d32c0702..cab6716c6 100644 --- a/lib/mutant/expression/descendants.rb +++ b/lib/mutant/expression/descendants.rb @@ -11,7 +11,8 @@ def syntax "descendants:#{const_name}" end - def matcher + # rubocop:disable Lint/UnusedMethodArgument + def matcher(env:) Matcher::Descendants.new(const_name: const_name) end end # Descendants diff --git a/lib/mutant/expression/method.rb b/lib/mutant/expression/method.rb index ecfcb6171..86968d26f 100644 --- a/lib/mutant/expression/method.rb +++ b/lib/mutant/expression/method.rb @@ -39,7 +39,9 @@ def initialize(*) # Matcher for expression # # @return [Matcher] - def matcher + # + # rubocop:disable Lint/UnusedMethodArgument + def matcher(env:) matcher_candidates = MATCHERS.fetch(scope_symbol) .map { |submatcher| submatcher.new(scope: scope) } diff --git a/lib/mutant/expression/methods.rb b/lib/mutant/expression/methods.rb index 89bd81432..ec4745f04 100644 --- a/lib/mutant/expression/methods.rb +++ b/lib/mutant/expression/methods.rb @@ -34,7 +34,9 @@ def initialize(*) # Matcher on expression # # @return [Matcher::Method] - def matcher + # + # rubocop:disable Lint/UnusedMethodArgument + def matcher(env:) matcher_candidates = MATCHERS.fetch(scope_symbol) .map { |submatcher| submatcher.new(scope: scope) } diff --git a/lib/mutant/expression/namespace.rb b/lib/mutant/expression/namespace.rb index 1319f2053..b47f2b343 100644 --- a/lib/mutant/expression/namespace.rb +++ b/lib/mutant/expression/namespace.rb @@ -34,7 +34,9 @@ def initialize(*) # Matcher for expression # # @return [Matcher] - def matcher + # + # rubocop:disable Lint/UnusedMethodArgument + def matcher(env:) Matcher::Namespace.new(expression: self) end @@ -65,7 +67,7 @@ class Exact < self # Matcher matcher on expression # # @return [Matcher] - def matcher + def matcher(env:) raw_scope = find_raw_scope if raw_scope diff --git a/lib/mutant/expression/source.rb b/lib/mutant/expression/source.rb new file mode 100644 index 000000000..126f807a0 --- /dev/null +++ b/lib/mutant/expression/source.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Mutant + class Expression + class Source < self + include Anima.new(:glob_expression) + + REGEXP = /\Asource:(?.+)\z/ + + def syntax + "source:#{glob_expression}" + end + + def matcher(env:) + matchers = scope_names(env: env).uniq.map do |scope_name| + Namespace::Recursive.new(scope_name: scope_name).matcher(env: nil) + end + + Matcher::Chain.new(matchers: matchers) + end + + private + + def scope_names(env:) + env.world.pathname.glob(glob_expression).flat_map do |path| + toplevel_consts(env.parser.call(path).node).map(&Unparser.public_method(:unparse)) + end + end + + def toplevel_consts(node) + children = node.children + + case node.type + when :class, :module + [children.fetch(0)] + when :begin + children.flat_map(&method(__method__)) + else + EMPTY_ARRAY + end + end + end # Source + end # Expression +end # Mutant diff --git a/lib/mutant/matcher.rb b/lib/mutant/matcher.rb index 7f58cce0f..2ac61cc34 100644 --- a/lib/mutant/matcher.rb +++ b/lib/mutant/matcher.rb @@ -15,13 +15,15 @@ class Matcher # Turn config into matcher # - # @param [Config] config + # @param [Env] env # # @return [Matcher] - def self.from_config(config) + def self.expand(env:) + matcher_config = env.config.matcher + Filter.new( - matcher: Chain.new(matchers: config.subjects.map(&:matcher)), - predicate: method(:allowed_subject?).curry.call(config) + matcher: Chain.new(matchers: matcher_config.subjects.map { |subject| subject.matcher(env: env) }), + predicate: method(:allowed_subject?).curry.call(matcher_config) ) end diff --git a/lib/mutant/parser.rb b/lib/mutant/parser.rb index 0a8d7e43d..f09a06869 100644 --- a/lib/mutant/parser.rb +++ b/lib/mutant/parser.rb @@ -18,7 +18,7 @@ def initialize # # @return [AST::Node] def call(path) - @cache[path] ||= parse(path.read) + @cache[path.expand_path] ||= parse(path.read) end private diff --git a/spec/unit/mutant/expression/descendants_spec.rb b/spec/unit/mutant/expression/descendants_spec.rb index a183b4081..f16cede52 100644 --- a/spec/unit/mutant/expression/descendants_spec.rb +++ b/spec/unit/mutant/expression/descendants_spec.rb @@ -6,7 +6,7 @@ describe '#matcher' do def apply - object.matcher + object.matcher(env: instance_double(Mutant::Env)) end it 'returns expected matcher' do diff --git a/spec/unit/mutant/expression/method_spec.rb b/spec/unit/mutant/expression/method_spec.rb index a35feb742..eab82a8fc 100644 --- a/spec/unit/mutant/expression/method_spec.rb +++ b/spec/unit/mutant/expression/method_spec.rb @@ -25,7 +25,7 @@ end describe '#matcher' do - subject { object.matcher } + subject { object.matcher(env: instance_double(Mutant::Env)) } context 'with an instance method' do let(:input) { instance_method } diff --git a/spec/unit/mutant/expression/methods_spec.rb b/spec/unit/mutant/expression/methods_spec.rb index ccaf76e63..91010e7a0 100644 --- a/spec/unit/mutant/expression/methods_spec.rb +++ b/spec/unit/mutant/expression/methods_spec.rb @@ -48,7 +48,7 @@ end describe '#matcher' do - subject { object.matcher } + subject { object.matcher(env: instance_double(Mutant::Env)) } let(:scope) do Mutant::Scope.new( diff --git a/spec/unit/mutant/expression/namespace/exact_spec.rb b/spec/unit/mutant/expression/namespace/exact_spec.rb index 47644a3c4..84eb3a18a 100644 --- a/spec/unit/mutant/expression/namespace/exact_spec.rb +++ b/spec/unit/mutant/expression/namespace/exact_spec.rb @@ -5,7 +5,7 @@ let(:input) { 'TestApp::Literal' } describe '#matcher' do - subject { object.matcher } + subject { object.matcher(env: instance_double(Mutant::Env)) } let(:scope) do Mutant::Scope.new( diff --git a/spec/unit/mutant/expression/namespace/recursive_spec.rb b/spec/unit/mutant/expression/namespace/recursive_spec.rb index 6fbba08d0..cc9aa4d28 100644 --- a/spec/unit/mutant/expression/namespace/recursive_spec.rb +++ b/spec/unit/mutant/expression/namespace/recursive_spec.rb @@ -5,7 +5,7 @@ let(:input) { 'TestApp::Literal*' } describe '#matcher' do - subject { object.matcher } + subject { object.matcher(env: instance_double(Mutant::Env)) } it { should eql(Mutant::Matcher::Namespace.new(expression: object)) } end diff --git a/spec/unit/mutant/expression/source_spec.rb b/spec/unit/mutant/expression/source_spec.rb new file mode 100644 index 000000000..fd69dc980 --- /dev/null +++ b/spec/unit/mutant/expression/source_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Expression::Source do + let(:object) { parse_expression(input) } + let(:input) { 'source:lib/**/*.rb' } + + describe '#matcher' do + def apply + object.matcher(env: env) + end + + let(:glob_expression) { 'lib/**/*.rb' } + let(:node_a) { s(:nil) } + let(:node_b) { s(:nil) } + let(:parser) { instance_double(Mutant::Parser) } + let(:path_a) { instance_double(Pathname, :a) } + let(:path_b) { instance_double(Pathname, :b) } + let(:pathname) { class_double(Pathname) } + let(:world) { instance_double(Mutant::World, pathname: pathname) } + + let(:path_asts) do + { + path_a => ast_a, + path_b => ast_b + } + end + + let(:config) do + instance_double( + Mutant::Config, + matcher: instance_double(Mutant::Matcher::Config, diffs: []) + ) + end + + let(:ast_a) do + Mutant::AST.new( + comment_associations: [], + node: node_a + ) + end + + let(:ast_b) do + Mutant::AST.new( + comment_associations: [], + node: node_b + ) + end + + let(:env) do + instance_double( + Mutant::Env, + config: config, + matchable_scopes: [scope_a, scope_b], + parser: parser, + world: world + ) + end + + let(:scope_a) do + Mutant::Scope.new( + expression: parse_expression('Foo::Bar'), + raw: Mutant + ) + end + + let(:scope_b) do + Mutant::Scope.new( + expression: parse_expression('Bar::Baz'), + raw: Mutant + ) + end + + before do + allow(pathname).to receive_messages(glob: glob_result) + allow(pathname).to receive(:new, &Pathname.method(:new)) + + allow(parser).to receive(:call) do |path| + path_asts.fetch(path) + end + end + + shared_examples 'performs glob' do + it 'performs glob' do + apply + + expect(pathname).to have_received(:glob).with(glob_expression) + end + end + + shared_examples 'no matchers' do + it 'returns no matches' do + expect(apply).to eql(Mutant::Matcher::Chain.new(matchers: [])) + end + end + + describe '#call' do + context 'when no files are matched' do + let(:glob_result) { [] } + + include_examples 'no matchers' + include_examples 'performs glob' + end + + context 'when files are matched' do + let(:glob_result) { [path_a, path_b] } + + context 'and files contain no matchable constants' do + include_examples 'no matchers' + include_examples 'performs glob' + end + + context 'and files contain single matchable constants' do + let(:node_a) { s(:module, s(:const, nil, :Foo)) } + + include_examples 'performs glob' + + it 'returns matcher' do + expect(apply).to eql( + Mutant::Matcher::Chain.new( + matchers: [ + Mutant::Matcher::Namespace.new(expression: parse_expression('Foo*')) + ] + ) + ) + end + end + + context 'and files contain multiple matchable constants' do + context 'without duplicates' do + include_examples 'performs glob' + + let(:node_a) do + s(:begin, + s(:class, s(:const, nil, :Foo), nil), + s(:module, s(:const, nil, :Bar))) + end + + it 'returns matcher' do + expect(apply).to eql( + Mutant::Matcher::Chain.new( + matchers: [ + Mutant::Matcher::Namespace.new(expression: parse_expression('Foo*')), + Mutant::Matcher::Namespace.new(expression: parse_expression('Bar*')) + ] + ) + ) + end + end + + context 'with duplicates' do + include_examples 'performs glob' + + let(:node_a) do + s(:begin, + s(:module, s(:const, nil, :Foo)), + s(:module, s(:const, nil, :Foo))) + end + + it 'returns matcher' do + expect(apply).to eql( + Mutant::Matcher::Chain.new( + matchers: [ + Mutant::Matcher::Namespace.new(expression: parse_expression('Foo*')) + ] + ) + ) + end + end + end + end + end + end + + describe '#syntax' do + def apply + object.syntax + end + + it 'returns input' do + expect(apply).to eql(input) + end + end + + describe '.try_parse' do + def apply + described_class.try_parse(input) + end + + context 'on valid input' do + it 'returns expected matcher' do + expect(apply).to eql(described_class.new(glob_expression: 'lib/**/*.rb')) + end + end + + context 'on invalid input' do + let(:input) { '' } + + it 'returns nil' do + expect(apply).to be(nil) + end + end + end +end diff --git a/spec/unit/mutant/matcher_spec.rb b/spec/unit/mutant/matcher_spec.rb index ded903c43..e740a3f29 100644 --- a/spec/unit/mutant/matcher_spec.rb +++ b/spec/unit/mutant/matcher_spec.rb @@ -1,18 +1,24 @@ # frozen_string_literal: true RSpec.describe Mutant::Matcher do - describe '.from_config' do + describe '.expand' do def apply - described_class.from_config(config) + described_class.expand(env: env) end let(:anon_matcher) { instance_double(Mutant::Matcher) } let(:diffs) { [] } - let(:env) { instance_double(Mutant::Env) } let(:expression_a) { expression('Foo::Bar#a', matcher_a) } let(:expression_b) { expression('Foo::Bar#b', matcher_b) } let(:ignore_expressions) { [] } + let(:env) do + instance_double( + Mutant::Env, + config: instance_double(Mutant::Config, matcher: config) + ) + end + let(:matcher_a) do instance_double(Mutant::Matcher, call: [subject_a]) end @@ -24,6 +30,7 @@ def apply let(:expression_class) do Class.new(Mutant::Expression) do include Unparser::Anima.new(:child, :matcher) + include Unparser::Equalizer.new %w[syntax prefix?].each do |name| define_method(name) do |*arguments, &block| @@ -31,7 +38,10 @@ def apply end end - public :matcher + define_method(:matcher) do |env:| + fail unless env + @matcher + end end end diff --git a/spec/unit/mutant/parser_spec.rb b/spec/unit/mutant/parser_spec.rb index cd52bdb57..4120cf003 100644 --- a/spec/unit/mutant/parser_spec.rb +++ b/spec/unit/mutant/parser_spec.rb @@ -4,7 +4,9 @@ let(:object) { described_class.new } describe '#call' do - let(:path) { instance_double(Pathname) } + let(:path) { instance_double(Pathname, expand_path: expanded_path) } + + let(:expanded_path) { instance_double(Pathname) } let(:expected_node) { s(:sym, :source) }