Add test minitest integration

[fix #92]
This commit is contained in:
Markus Schirp 2018-11-18 21:25:07 +00:00
parent 1047e0e660
commit 9ceb2bd650
10 changed files with 298 additions and 3 deletions

View file

@ -1,3 +1,3 @@
--- ---
threshold: 16 threshold: 16
total_score: 1364 total_score: 1395

View file

@ -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>] 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<Test>]
def all_tests
all_tests_index.keys
end
memoize :all_tests
private
# The index of all tests to runnable test cases
#
# @return [Hash<Test,TestCase>]
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<TestCase>]
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<TestCase>]
#
# ignore :reek:UtilityFunction
def test_case(runnable)
runnable.runnable_methods.map { |method| TestCase.new(runnable, method) }
end
end # Minitest
end # Integration
end # Mutant

View file

@ -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)

22
mutant-minitest.gemspec Normal file
View file

@ -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

View file

@ -14,9 +14,9 @@ Gem::Specification.new do |gem|
gem.require_paths = %w[lib] 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.test_files = `git ls-files -- spec/{unit,integration}`.split("\n")
gem.extra_rdoc_files = %w[LICENSE] gem.extra_rdoc_files = %w[LICENSE]
gem.executables = %w[mutant] gem.executables = %w[mutant]

View file

@ -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

View file

@ -41,6 +41,16 @@
mutation_generation: true mutation_generation: true
expected_errors: {} expected_errors: {}
exclude: [] 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 - name: axiom
namespace: Axiom namespace: Axiom
repo_uri: 'https://github.com/dkubb/axiom.git' repo_uri: 'https://github.com/dkubb/axiom.git'

View file

@ -167,6 +167,7 @@ module MutantSpec
repo_path.join('Gemfile').open('a') do |file| repo_path.join('Gemfile').open('a') do |file|
file << "gem 'mutant', path: '#{relative}'\n" file << "gem 'mutant', path: '#{relative}'\n"
file << "gem 'mutant-rspec', 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" file << "eval_gemfile File.expand_path('#{relative.join('Gemfile.shared')}')\n"
end end
lockfile = repo_path.join('Gemfile.lock') lockfile = repo_path.join('Gemfile.lock')

View file

@ -0,0 +1,6 @@
source 'https://rubygems.org'
gem 'minitest', '~> 5.11'
gem 'mutant', path: '../'
gem 'mutant-minitest', path: '../'
gem 'adamantium'

View file

@ -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