diff --git a/.circleci/config.yml b/.circleci/config.yml index 6402c75f9..8e5f02696 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,12 @@ jobs: - checkout - run: bundle install - run: bundle exec rspec spec/unit + integration_minitest: + <<: *defaults + steps: + - checkout + - run: bundle install + - run: bundle exec rspec spec/integration -e minitest integration_rspec: <<: *defaults steps: @@ -21,7 +27,7 @@ jobs: steps: - checkout - run: bundle install - - run: bundle exec rspec spec/integration -e 'generation' + - run: bundle exec rspec spec/integration -e generation metrics: <<: *defaults steps: diff --git a/config/flay.yml b/config/flay.yml index 33a9a044d..0048c39b0 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 16 -total_score: 1364 +total_score: 1395 diff --git a/lib/mutant/integration/minitest.rb b/lib/mutant/integration/minitest.rb new file mode 100644 index 000000000..3a97156b2 --- /dev/null +++ b/lib/mutant/integration/minitest.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'minitest' +require 'mutant/minitest/coverage' + +module Minitest + # Prevent autorun from running tests when the VM closes + # + # Mutant needs control about the exit status of the VM and + # the moment of test execution + # + # @api private + # + # @return [nil] + def self.autorun; end +end # Minitest + +module Mutant + class Integration + # Minitest integration + class Minitest < self + TEST_FILE_PATTERN = './test/**/{test_*,*_test}.rb' + IDENTIFICATION_FORMAT = 'minitest:%s#%s' + + private_constant(*constants(false)) + + # Compose a runnable with test method + # + # This looks actually like a missing object on minitest implementation. + class TestCase + include Adamantium, Concord.new(:klass, :test_method) + + # Identification string + # + # @return [String] + def identification + IDENTIFICATION_FORMAT % [klass, test_method] + end + memoize :identification + + # Run test case + # + # @param [Object] reporter + # + # @return [Boolean] + def call(reporter) + ::Minitest::Runnable.run_one_method(klass, test_method, reporter) + reporter.passed? + end + + # Cover expression syntaxes + # + # @return [String, nil] + def expression_syntax + klass.resolve_cover_expression + end + + end # TestCase + + private_constant(*constants(false)) + + # Setup integration + # + # @return [self] + def setup + Pathname.glob(TEST_FILE_PATTERN) + .map(&:to_s) + .each(&method(:require)) + + self + end + + # Call test integration + # + # @param [Array] tests + # + # @return [Result::Test] + # + # rubocop:disable MethodLength + # + # ignore :reek:TooManyStatements + def call(tests) + test_cases = tests.map(&all_tests_index.method(:fetch)) + output = StringIO.new + start = Time.now + + reporter = ::Minitest::SummaryReporter.new(output) + + reporter.start + + test_cases.each do |test| + break unless test.call(reporter) + end + + output.rewind + + Result::Test.new( + passed: reporter.passed?, + tests: tests, + output: output.read, + runtime: Time.now - start + ) + end + + # All tests exposed by this integration + # + # @return [Array] + def all_tests + all_tests_index.keys + end + memoize :all_tests + + private + + # The index of all tests to runnable test cases + # + # @return [Hash] + def all_tests_index + all_test_cases.each_with_object({}) do |test_case, index| + index[construct_test(test_case)] = test_case + end + end + memoize :all_tests_index + + # Construct test from test case + # + # @param [TestCase] + # + # @return [Test] + def construct_test(test_case) + Test.new( + id: test_case.identification, + expression: config.expression_parser.call(test_case.expression_syntax) + ) + end + + # All minitest test cases + # + # Intentional utility method. + # + # @return [Array] + def all_test_cases + ::Minitest::Runnable + .runnables + .select(&method(:allow_runnable?)) + .flat_map(&method(:test_case)) + end + + # Test if runnable qualifies for mutation testing + # + # @param [Class] + # + # @return [Bool] + # + # ignore :reek:UtilityFunction + def allow_runnable?(klass) + !klass.equal?(::Minitest::Runnable) && klass.resolve_cover_expression + end + + # Turn a minitest runnable into its test cases + # + # Intentional utility method. + # + # @param [Object] runnable + # + # @return [Array] + # + # ignore :reek:UtilityFunction + def test_case(runnable) + runnable.runnable_methods.map { |method| TestCase.new(runnable, method) } + end + end # Minitest + end # Integration +end # Mutant diff --git a/lib/mutant/minitest/coverage.rb b/lib/mutant/minitest/coverage.rb new file mode 100644 index 000000000..501f9c208 --- /dev/null +++ b/lib/mutant/minitest/coverage.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'minitest' + +module Mutant + module Minitest + module Coverage + # Setup coverage declaration for current class + # + # @param [String] + # + # @example + # + # class MyTest < MiniTest::Test + # cover 'MyCode*' + # + # def test_some_stuff + # end + # end + # + # @api public + def cover(expression) + fail "#{self} already declares to cover: #{@covers}" if @covers + + @cover_expression = expression + end + + # Effective coverage expression + # + # @return [String, nil] + # + # @api private + def resolve_cover_expression + return @cover_expression if defined?(@cover_expression) + + try_superclass_cover_expression + end + + private + + # Attempt to resolve superclass cover expressio + # + # @return [String, nil] + # + # @api private + def try_superclass_cover_expression + return if superclass.equal?(::Minitest::Runnable) + + superclass.resolve_cover_expression + end + + end # Coverage + end # Minitest +end # Mutant + +Minitest::Test.extend(Mutant::Minitest::Coverage) diff --git a/mutant-minitest.gemspec b/mutant-minitest.gemspec new file mode 100644 index 000000000..dff804b5a --- /dev/null +++ b/mutant-minitest.gemspec @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require File.expand_path('lib/mutant/version', __dir__) + +Gem::Specification.new do |gem| + gem.name = 'mutant-minitest' + gem.version = Mutant::VERSION.dup + gem.authors = ['Markus Schirp'] + gem.email = %w[mbj@schirp-dso.com] + gem.description = 'Minitest integration for mutant' + gem.summary = gem.description + gem.homepage = 'https://github.com/mbj/mutant' + gem.license = 'MIT' + + gem.require_paths = %w[lib] + gem.files = `git ls-files -- lib/mutant/{minitest,/integration/minitest.rb}`.split("\n") + gem.test_files = `git ls-files -- spec/integration/mutant/minitest.rb`.split("\n") + gem.extra_rdoc_files = %w[LICENSE] + + gem.add_runtime_dependency('minitest', '~> 5.11') + gem.add_runtime_dependency('mutant', "~> #{gem.version}") +end diff --git a/mutant.gemspec b/mutant.gemspec index 0e3c072a9..c90b277d9 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -14,9 +14,9 @@ Gem::Specification.new do |gem| gem.require_paths = %w[lib] - mutant_integration_files = `git ls-files -- lib/mutant/integration/*.rb`.split("\n") + exclusion = `git ls-files -- lib/mutant/{minitest,integration}`.split("\n") - gem.files = `git ls-files`.split("\n") - mutant_integration_files + gem.files = `git ls-files`.split("\n") - exclusion gem.test_files = `git ls-files -- spec/{unit,integration}`.split("\n") gem.extra_rdoc_files = %w[LICENSE] gem.executables = %w[mutant] diff --git a/spec/integration/mutant/minitest_spec.rb b/spec/integration/mutant/minitest_spec.rb new file mode 100644 index 000000000..b51456979 --- /dev/null +++ b/spec/integration/mutant/minitest_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.describe 'minitest integration', mutant: false do + + let(:base_cmd) { 'bundle exec mutant -I test -I lib --require test_app --use minitest' } + + let(:gemfile) { 'Gemfile.minitest' } + + it_behaves_like 'framework integration' +end diff --git a/spec/integrations.yml b/spec/integrations.yml index 983301316..354296ddf 100644 --- a/spec/integrations.yml +++ b/spec/integrations.yml @@ -41,6 +41,16 @@ mutation_generation: true expected_errors: {} exclude: [] +- name: auom + namespace: AUOM + repo_uri: 'https://github.com/mbj/auom.git' + repo_ref: 'origin/add/minitest' + ruby_glob_pattern: '**/*.rb' + integration: minitest + mutation_coverage: true + mutation_generation: true + expected_errors: {} + exclude: [] - name: axiom namespace: Axiom repo_uri: 'https://github.com/dkubb/axiom.git' diff --git a/spec/support/corpus.rb b/spec/support/corpus.rb index 137287310..45245dde9 100644 --- a/spec/support/corpus.rb +++ b/spec/support/corpus.rb @@ -167,6 +167,7 @@ def install_mutant repo_path.join('Gemfile').open('a') do |file| file << "gem 'mutant', path: '#{relative}'\n" file << "gem 'mutant-rspec', path: '#{relative}'\n" + file << "gem 'mutant-minitest', path: '#{relative}'\n" file << "eval_gemfile File.expand_path('#{relative.join('Gemfile.shared')}')\n" end lockfile = repo_path.join('Gemfile.lock') diff --git a/test_app/Gemfile.minitest b/test_app/Gemfile.minitest new file mode 100644 index 000000000..9e889b4e9 --- /dev/null +++ b/test_app/Gemfile.minitest @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gem 'minitest', '~> 5.11' +gem 'mutant', path: '../' +gem 'mutant-minitest', path: '../' +gem 'adamantium' diff --git a/test_app/test/unit/test_app/literal_test.rb b/test_app/test/unit/test_app/literal_test.rb new file mode 100644 index 000000000..52837f56a --- /dev/null +++ b/test_app/test/unit/test_app/literal_test.rb @@ -0,0 +1,16 @@ +require 'minitest/autorun' +require 'mutant/minitest/coverage' + +class LiteralTest < Minitest::Test + cover 'TestApp::Literal*' + + def test_command + object = ::TestApp::Literal.new + + assert_equal(object, object.command('x')) + end + + def test_string + assert_equal('string', ::TestApp::Literal.new.string) + end +end