From 9fc563a48baabdbacc1255d0ae43e9fa2f7d5c37 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 13 Jul 2015 01:06:39 +0000 Subject: [PATCH] Add repository diff based subject filter --- config/flay.yml | 2 +- lib/mutant.rb | 1 + lib/mutant/repository.rb | 66 +++++++++++++++++++ spec/unit/mutant/repository/diff_spec.rb | 57 ++++++++++++++++ .../mutant/repository/subject_filter_spec.rb | 28 ++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 lib/mutant/repository.rb create mode 100644 spec/unit/mutant/repository/diff_spec.rb create mode 100644 spec/unit/mutant/repository/subject_filter_spec.rb diff --git a/config/flay.yml b/config/flay.yml index fdafaff5..9b20be07 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 18 -total_score: 1173 +total_score: 1189 diff --git a/lib/mutant.rb b/lib/mutant.rb index 8348d9f9..fd254c47 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -174,6 +174,7 @@ require 'mutant/reporter/cli/printer/status_progressive' require 'mutant/reporter/cli/printer/test_result' require 'mutant/reporter/cli/tput' require 'mutant/reporter/cli/format' +require 'mutant/repository' require 'mutant/zombifier' module Mutant diff --git a/lib/mutant/repository.rb b/lib/mutant/repository.rb new file mode 100644 index 00000000..1ad52c0a --- /dev/null +++ b/lib/mutant/repository.rb @@ -0,0 +1,66 @@ +module Mutant + module Repository + # Error raised on repository interaction problems + RepositoryError = Class.new(RuntimeError) + + # Subject filter based on repository diff + class SubjectFilter + include Adamantium, Concord.new(:diff) + + # Test if subject was touched in diff + # + # @param [Subject] subject + # + # @return [Boolean] + # + # @api private + def call(subject) + diff.touches?(subject.source_path, subject.source_lines) + end + + end # SubjectFilter + + # Diff between two objects in repository + class Diff + include Adamantium, Concord.new(:from, :to) + + HEAD = 'HEAD'.freeze + private_constant(*constants(false)) + + # Create diff from head to revision + # + # @return [Diff] + # + # @api private + def self.from_head(to) + new(HEAD, to) + end + + # Test if diff changes file at line range + # + # @param [Pathname] path + # @param [Range] line_range + # + # @return [Boolean] + # + # @raise [RepositoryError] + # when git command failed + # + # @api private + def touches?(path, line_range) + command = %W[ + git log --pretty=oneline + #{from}...#{to} + -L #{line_range.begin},#{line_range.end}:#{path} + ] + + stdout, status = Open3.capture2(*command, binmode: true) + + fail RepositoryError, "Command #{command} failed!" unless status.success? + + !stdout.empty? + end + + end # Diff + end # Repository +end # Mutant diff --git a/spec/unit/mutant/repository/diff_spec.rb b/spec/unit/mutant/repository/diff_spec.rb new file mode 100644 index 00000000..e2bb0bae --- /dev/null +++ b/spec/unit/mutant/repository/diff_spec.rb @@ -0,0 +1,57 @@ +describe Mutant::Repository::Diff do + describe '.from_head' do + subject { described_class.from_head(to_revision) } + + let(:to_revision) { double('to revision') } + + it { should eql(described_class.new('HEAD', to_revision)) } + end + + describe '#touches?' do + let(:object) { described_class.new('from_rev', 'to_rev') } + let(:path) { Pathname.new('foo.rb') } + let(:line_range) { 1..2 } + let(:status) { double('Status', success?: success?) } + let(:stdout) { double('Stdout', empty?: stdout_empty?) } + let(:stdout_empty?) { false } + + subject { object.touches?(path, line_range) } + + before do + expect(Open3).to receive(:capture2).with( + *expected_command, + binmode: true + ).and_return([stdout, status]) + end + + let(:expected_command) do + %w[ + git log --pretty=oneline from_rev...to_rev -L 1,2:foo.rb + ] + end + + context 'on failure of git command' do + let(:success?) { false } + + it 'raises error' do + expect { subject }.to raise_error(Mutant::Repository::RepositoryError, "Command #{expected_command} failed!") + end + end + + context 'on suuccess of git command' do + let(:success?) { true } + + context 'on empty stdout' do + let(:stdout_empty?) { true } + + it { should be(false) } + end + + context 'on non empty stdout' do + let(:stdout_empty?) { false } + + it { should be(true) } + end + end + end +end diff --git a/spec/unit/mutant/repository/subject_filter_spec.rb b/spec/unit/mutant/repository/subject_filter_spec.rb new file mode 100644 index 00000000..f30ce46a --- /dev/null +++ b/spec/unit/mutant/repository/subject_filter_spec.rb @@ -0,0 +1,28 @@ +RSpec.describe Mutant::Repository::SubjectFilter do + context '#call' do + subject { object.call(mutant_subject) } + + let(:object) { described_class.new(diff) } + let(:diff) { double('Diff') } + let(:return_value) { double('Return Value') } + + let(:mutant_subject) do + double( + 'Subject', + source_path: double('source path'), + source_lines: double('source lines') + ) + end + + before do + expect(diff).to receive(:touches?).with( + mutant_subject.source_path, + mutant_subject.source_lines + ).and_return(return_value) + end + + it 'connects return value to repository diff API' do + expect(subject).to be(return_value) + end + end +end