From fc2a98a74aea9d2481d2c680f96902a8bc958e76 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Fri, 1 Mar 2019 23:19:18 +0000 Subject: [PATCH 1/2] Fix typo --- lib/mutant/selector.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mutant/selector.rb b/lib/mutant/selector.rb index 2d776e95f..0a07bc72b 100644 --- a/lib/mutant/selector.rb +++ b/lib/mutant/selector.rb @@ -7,7 +7,7 @@ class Selector # Tests for subject # - # @param [Subject] subjecto + # @param [Subject] subject # # @return [Enumerable] abstract_method :call From 0e0d7bc820ad4355c1c4d1818c5f3be0b023f6bc Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Sun, 3 Mar 2019 21:20:14 +0000 Subject: [PATCH 2/2] [WIP] Add tracing --- lib/mutant.rb | 3 + lib/mutant/base.rb | 23 +++++- lib/mutant/env.rb | 2 +- lib/mutant/integration.rb | 6 +- lib/mutant/integration/rspec.rb | 10 ++- lib/mutant/line_trace.rb | 17 ++++ lib/mutant/runner.rb | 82 +++++++++++++++++++- lib/mutant/selector.rb | 2 +- lib/mutant/selector/expression.rb | 7 +- lib/mutant/selector/intersection.rb | 23 ++++++ lib/mutant/selector/null.rb | 4 +- lib/mutant/selector/trace.rb | 22 ++++++ lib/mutant/subject.rb | 8 ++ lib/mutant/test.rb | 16 +++- spec/unit/mutant/env_spec.rb | 47 +++++++---- spec/unit/mutant/integration/rspec_spec.rb | 34 ++++---- spec/unit/mutant/selector/expression_spec.rb | 10 +-- spec/unit/mutant/selector/null_spec.rb | 2 +- 18 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 lib/mutant/line_trace.rb create mode 100644 lib/mutant/selector/intersection.rb create mode 100644 lib/mutant/selector/trace.rb diff --git a/lib/mutant.rb b/lib/mutant.rb index 78ca053f8..7a18892b6 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -155,7 +155,9 @@ module Mutant require 'mutant/integration/null' require 'mutant/selector' require 'mutant/selector/expression' +require 'mutant/selector/intersection' require 'mutant/selector/null' +require 'mutant/selector/trace' require 'mutant/config' require 'mutant/cli' require 'mutant/color' @@ -186,6 +188,7 @@ module Mutant require 'mutant/variable' require 'mutant/zombifier' require 'mutant/range' +require 'mutant/line_trace' module Mutant WORLD = World.new( diff --git a/lib/mutant/base.rb b/lib/mutant/base.rb index 58968bd68..32f1c4c0d 100644 --- a/lib/mutant/base.rb +++ b/lib/mutant/base.rb @@ -52,6 +52,20 @@ def fmap(&block) def apply(&block) require_block(&block) end + + # Unwrap just + # + # @return [Object] + # + # rubocop:disable Style/GuardClause + def from_just + if block_given? + yield + else + fail "Expected just value, got #{inspect}" + end + end + # rubocop:enable Style/GuardClause end # Nothing class Just < self @@ -66,10 +80,17 @@ def fmap # Evalute applicative block # - # @return [Maybe] + # @return [Maybe] def apply yield(value) end + + # Unwrap just + # + # @return [Object] + def from_just + value + end end # Just end # Maybe diff --git a/lib/mutant/env.rb b/lib/mutant/env.rb index 62810b6e2..bfaf39401 100644 --- a/lib/mutant/env.rb +++ b/lib/mutant/env.rb @@ -59,7 +59,7 @@ def kill(mutation) # @return Hash{Mutation => Enumerable} def selections subjects.map do |subject| - [subject, selector.call(subject)] + [subject, selector.call(subject).from_just { EMPTY_ARRAY }] end.to_h end memoize :selections diff --git a/lib/mutant/integration.rb b/lib/mutant/integration.rb index 0cd4e29eb..11e89b039 100644 --- a/lib/mutant/integration.rb +++ b/lib/mutant/integration.rb @@ -23,7 +23,7 @@ class Integration # Setup integration # - # @param env [Bootstrap] + # @param env [Env] # # @return [Either] def self.setup(env) @@ -34,7 +34,7 @@ def self.setup(env) # Attempt to require integration # - # @param env [Bootstrap] + # @param env [Env] # # @return [Either] # @@ -58,7 +58,7 @@ def self.attempt_require(env) # Attempt const get # - # @param env [Boostrap] + # @param env [Env] # # @return [Either>] # diff --git a/lib/mutant/integration/rspec.rb b/lib/mutant/integration/rspec.rb index 1da290a21..3e82aea64 100644 --- a/lib/mutant/integration/rspec.rb +++ b/lib/mutant/integration/rspec.rb @@ -99,15 +99,21 @@ def all_tests_index def parse_example(example, index) metadata = example.metadata + location = metadata.fetch(:location) + + path, lineno = location.split(':', 2) + id = TEST_ID_FORMAT % { index: index, - location: metadata.fetch(:location), + location: location, description: metadata.fetch(:full_description) } Test.new( expression: parse_expression(metadata), - id: id + id: id, + lineno: Integer(lineno), + path: Pathname.pwd.join(path).to_s ) end diff --git a/lib/mutant/line_trace.rb b/lib/mutant/line_trace.rb new file mode 100644 index 000000000..d20f6151b --- /dev/null +++ b/lib/mutant/line_trace.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mutant + # Line tracer + module LineTrace + # Run line tracer + def self.call(keep, &block) + lines = [] + + result = TracePoint.new(:b_call, :call, :line) do |trace| + lines << "#{trace.path}:#{trace.lineno}" if keep.call(trace) + end.enable(&block) + + [result, lines.freeze].freeze + end + end # LineTrace +end # Mutant diff --git a/lib/mutant/runner.rb b/lib/mutant/runner.rb index 3afc82294..851d2da86 100644 --- a/lib/mutant/runner.rb +++ b/lib/mutant/runner.rb @@ -9,11 +9,15 @@ module Runner def self.apply(env) reporter(env).start(env) - Either::Right.new(run_mutation_analysis(env)) + run_tracing(env).fmap do |env| + run_mutation_analysis(env) + end end # Run mutation analysis # + # @param [Env] env + # # @return [undefined] def self.run_mutation_analysis(env) reporter = reporter(env) @@ -27,6 +31,82 @@ def self.run_mutation_analysis(env) end private_class_method :run_mutation_analysis + # Run tracing + def self.run_tracing(env) + integration = env.integration + tests = integration.all_tests + + subject_paths = env.subjects.map(&:source_path).map(&:to_s).to_set + paths = subject_paths + tests.map(&:path) + + result = env.config.isolation.call do + LineTrace.call(->(trace) { paths.include?(trace.path) }) do + integration.call(tests) + end + end + + if result.success? + test_result, traces = result.value + + if test_result.passed + trace_selector(env, traces) + else + Either::Left.new('Trace tests did not pass! %s' % test_result.output) + end + else + Either::Left.new(trace_failure(result)) + end + end + + # Create trace selector + # + # @param [Env] env + # @param [Array] traces + # + # @return [Either] + def self.trace_selector(env, traces) + all_tests = env.integration.all_tests + + trace_tests = {} + test_traces = {} + + all_tests.each do |test| + (trace_tests[test.trace_location] ||= Set.new) << test + end + + current_traces = Set.new + + traces.each do |trace| + tests = trace_tests[trace] + + if tests + current_traces = tests.map do |test| + test_traces[test.id] ||= Set.new + end + end + + current_traces.each { |test_trace| test_trace << trace } + end + + missing = all_tests.count { |test| !test_traces.key?(test.id) } + + if missing.zero? + Either::Right.new( + env.with( + selector: Selector::Intersection.new( + [ + env.selector, + Selector::Trace.new(all_tests, test_traces) + ] + ) + ) + ) + else + Either::Left.new("total: #{all_tests.length} missing: #{missing}, found: #{test_traces.length} tests in traces") + end + end + private_class_method :trace_selector + # Run driver # # @param [Reporter] reporter diff --git a/lib/mutant/selector.rb b/lib/mutant/selector.rb index 0a07bc72b..08c7662f4 100644 --- a/lib/mutant/selector.rb +++ b/lib/mutant/selector.rb @@ -9,7 +9,7 @@ class Selector # # @param [Subject] subject # - # @return [Enumerable] + # @return [Maybe>] abstract_method :call end # Selector diff --git a/lib/mutant/selector/expression.rb b/lib/mutant/selector/expression.rb index 8392e3a30..912ab6c18 100644 --- a/lib/mutant/selector/expression.rb +++ b/lib/mutant/selector/expression.rb @@ -10,16 +10,17 @@ class Expression < self # # @param [Subject] subject # - # @return [Enumerable] + # @return [Maybe>] def call(subject) subject.match_expressions.each do |match_expression| subject_tests = integration.all_tests.select do |test| match_expression.prefix?(test.expression) end - return subject_tests if subject_tests.any? + + return Maybe::Just.new(subject_tests) if subject_tests.any? end - EMPTY_ARRAY + Maybe::Just.new(EMPTY_ARRAY) end end # Expression diff --git a/lib/mutant/selector/intersection.rb b/lib/mutant/selector/intersection.rb new file mode 100644 index 000000000..1936ecdeb --- /dev/null +++ b/lib/mutant/selector/intersection.rb @@ -0,0 +1,23 @@ +module Mutant + class Selector + # Selector that returns the intersection of tests returned by downstreadm selectors + class Intersection < self + include Adamantium::Flat, Concord.new(:selectors) + + # Test selected for subject + # + # @param [Subject] + # + # @return [Maybe>] + def call(subject) + selections = selectors.map { |selector| selector.call(subject) } + + Maybe::Just.new( + selections.reduce do |left, right| + left.from_just { return Maybe::Nothing.new } & right.from_just { return Maybe::Nothing.new } + end + ) + end + end # Intersection + end # Selector +end # Mutant diff --git a/lib/mutant/selector/null.rb b/lib/mutant/selector/null.rb index 7dc51c30d..b7dadf52c 100644 --- a/lib/mutant/selector/null.rb +++ b/lib/mutant/selector/null.rb @@ -10,9 +10,9 @@ class Null < self # # @param [Subject] subject # - # @return [Enumerable] + # @return [Maybe>] def call(_subject) - EMPTY_ARRAY + Maybe::Just.new(EMPTY_ARRAY) end end # Null end # Selector diff --git a/lib/mutant/selector/trace.rb b/lib/mutant/selector/trace.rb new file mode 100644 index 000000000..265401ace --- /dev/null +++ b/lib/mutant/selector/trace.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutant + class Selector + class Trace < self + include Concord.new(:tests, :test_traces) + + # Tests for subject + # + # @param [Subject] subject + # + # @return [Maybe>] + def call(subject) + Maybe::Just.new( + tests.select do |test| + test_traces.fetch(test.id).include?(subject.trace_location) + end + ) + end + end # Trace + end # Selector +end # Mutant diff --git a/lib/mutant/subject.rb b/lib/mutant/subject.rb index accdc7bde..8bfd03fd9 100644 --- a/lib/mutant/subject.rb +++ b/lib/mutant/subject.rb @@ -19,6 +19,14 @@ def mutations end memoize :mutations + # Trace location + # + # @return [String] + def trace_location + "#{source_path}:#{source_line}" + end + memoize :trace_location + # Source path # # @return [Pathname] diff --git a/lib/mutant/test.rb b/lib/mutant/test.rb index 39626392a..01d794e18 100644 --- a/lib/mutant/test.rb +++ b/lib/mutant/test.rb @@ -5,7 +5,9 @@ module Mutant class Test include Adamantium::Flat, Anima.new( :expression, - :id + :id, + :lineno, + :path ) # Identification string @@ -13,5 +15,17 @@ class Test # @return [String] alias_method :identification, :id + # Trace location + # + # @return [String] + def trace_location + "#{path}:#{lineno}" + end + memoize :trace_location + + def <=>(other) + id <=> other.id + end + end # Test end # Mutant diff --git a/spec/unit/mutant/env_spec.rb b/spec/unit/mutant/env_spec.rb index 1561a8eaf..654d498d8 100644 --- a/spec/unit/mutant/env_spec.rb +++ b/spec/unit/mutant/env_spec.rb @@ -14,16 +14,18 @@ ) end - let(:integration_class) { Mutant::Integration::Null } - let(:isolation) { Mutant::Isolation::None.new } - let(:kernel) { instance_double(Object, 'kernel') } - let(:subject_a) { instance_double(Mutant::Subject, :a) } - let(:subject_b) { instance_double(Mutant::Subject, :b) } - let(:reporter) { instance_double(Mutant::Reporter) } - let(:selector) { instance_double(Mutant::Selector) } - let(:test_a) { instance_double(Mutant::Test, :a) } - let(:test_b) { instance_double(Mutant::Test, :b) } - let(:test_c) { instance_double(Mutant::Test, :c) } + let(:integration_class) { Mutant::Integration::Null } + let(:isolation) { Mutant::Isolation::None.new } + let(:kernel) { instance_double(Object, 'kernel') } + let(:reporter) { instance_double(Mutant::Reporter) } + let(:selector) { instance_double(Mutant::Selector) } + let(:subject_a) { instance_double(Mutant::Subject, :a) } + let(:subject_b) { instance_double(Mutant::Subject, :b) } + let(:test_a) { instance_double(Mutant::Test, :a) } + let(:test_b) { instance_double(Mutant::Test, :b) } + let(:test_c) { instance_double(Mutant::Test, :c) } + let(:selector_result_a) { Mutant::Maybe::Just.new([test_a, test_b]) } + let(:selector_result_b) { Mutant::Maybe::Just.new([test_b, test_c]) } let(:integration) do instance_double(Mutant::Integration, all_tests: [test_a, test_b, test_c]) @@ -55,11 +57,11 @@ before do allow(selector).to receive(:call) .with(subject_a) - .and_return([test_a, test_b]) + .and_return(selector_result_a) allow(selector).to receive(:call) .with(subject_b) - .and_return([test_b, test_c]) + .and_return(selector_result_b) allow(Mutant::Timer).to receive(:now).and_return(2.0, 3.0) end @@ -135,11 +137,22 @@ def apply subject.selections end - it 'returns expected selections' do - expect(apply).to eql( - subject_a => [test_a, test_b], - subject_b => [test_b, test_c] - ) + context 'when selector returns values' do + it 'returns expected selections' do + expect(apply).to eql( + subject_a => [test_a, test_b], + subject_b => [test_b, test_c] + ) + end + end + + context 'when selector fails to return values' do + let(:selector_result_a) { Mutant::Maybe::Nothing.new } + let(:selector_result_b) { Mutant::Maybe::Nothing.new } + + it 'returns expected selections' do + expect(apply).to eql(subject_a => [], subject_b => []) + end end end diff --git a/spec/unit/mutant/integration/rspec_spec.rb b/spec/unit/mutant/integration/rspec_spec.rb index 70e4ad2c3..8dae123c4 100644 --- a/spec/unit/mutant/integration/rspec_spec.rb +++ b/spec/unit/mutant/integration/rspec_spec.rb @@ -12,7 +12,7 @@ double( 'Example A', metadata: { - location: 'example-a-location', + location: 'example-a-location:1', full_description: 'example-a-full-description' } ) @@ -22,7 +22,7 @@ double( 'Example B', metadata: { - location: 'example-b-location', + location: 'example-b-location:2', full_description: 'example-b-full-description', mutant: false } @@ -33,7 +33,7 @@ double( 'Example C', metadata: { - location: 'example-c-location', + location: 'example-c-location:3', full_description: 'Example::C blah' } ) @@ -43,7 +43,7 @@ double( 'Example D', metadata: { - location: 'example-d-location', + location: 'example-d-location:4', full_description: "Example::D\nblah" } ) @@ -53,7 +53,7 @@ double( 'Example E', metadata: { - location: 'example-e-location', + location: 'example-e-location:5', full_description: 'Example::E', mutant_expression: 'Foo' } @@ -98,20 +98,28 @@ let(:all_tests) do [ Mutant::Test.new( - id: 'rspec:0:example-a-location/example-a-full-description', - expression: parse_expression('*') + expression: parse_expression('*'), + id: 'rspec:0:example-a-location:1/example-a-full-description', + lineno: 1, + path: Pathname.pwd.join('example-a-location').to_s ), Mutant::Test.new( - id: 'rspec:1:example-c-location/Example::C blah', - expression: parse_expression('Example::C') + expression: parse_expression('Example::C'), + id: 'rspec:1:example-c-location:3/Example::C blah', + lineno: 3, + path: Pathname.pwd.join('example-c-location').to_s ), Mutant::Test.new( - id: "rspec:2:example-d-location/Example::D\nblah", - expression: parse_expression('*') + expression: parse_expression('*'), + id: "rspec:2:example-d-location:4/Example::D\nblah", + lineno: 4, + path: Pathname.pwd.join('example-d-location').to_s ), Mutant::Test.new( - id: 'rspec:3:example-e-location/Example::E', - expression: parse_expression('Foo') + expression: parse_expression('Foo'), + id: 'rspec:3:example-e-location:5/Example::E', + lineno: 5, + path: Pathname.pwd.join('example-e-location').to_s ) ] end diff --git a/spec/unit/mutant/selector/expression_spec.rb b/spec/unit/mutant/selector/expression_spec.rb index fa725d851..b06516bba 100644 --- a/spec/unit/mutant/selector/expression_spec.rb +++ b/spec/unit/mutant/selector/expression_spec.rb @@ -31,31 +31,31 @@ context 'without available tests' do let(:all_tests) { [] } - it { should eql([]) } + it { should eql(Mutant::Maybe::Just.new([])) } end context 'without qualifying tests' do let(:all_tests) { [test_c] } - it { should eql([]) } + it { should eql(Mutant::Maybe::Just.new([])) } end context 'with qualifying tests for first match expression' do let(:all_tests) { [test_a] } - it { should eql([test_a]) } + it { should eql(Mutant::Maybe::Just.new([test_a])) } end context 'with qualifying tests for second match expression' do let(:all_tests) { [test_b] } - it { should eql([test_b]) } + it { should eql(Mutant::Maybe::Just.new([test_b])) } end context 'with qualifying tests for the first and second match expression' do let(:all_tests) { [test_a, test_b] } - it { should eql([test_a]) } + it { should eql(Mutant::Maybe::Just.new([test_a])) } end end end diff --git a/spec/unit/mutant/selector/null_spec.rb b/spec/unit/mutant/selector/null_spec.rb index 2063e7e4d..4757e670e 100644 --- a/spec/unit/mutant/selector/null_spec.rb +++ b/spec/unit/mutant/selector/null_spec.rb @@ -11,7 +11,7 @@ def apply end it 'returns no tests' do - expect(apply).to eql([]) + expect(apply).to eql(Mutant::Maybe::Just.new([])) end end end