Skip to content

Commit

Permalink
Add test minitest integration
Browse files Browse the repository at this point in the history
[fix #92]
  • Loading branch information
mbj committed Sep 22, 2015
1 parent 3a67616 commit 11474b6
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 2 deletions.
2 changes: 1 addition & 1 deletion config/flay.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1177
total_score: 1207
2 changes: 2 additions & 0 deletions config/reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ TooManyStatements:
- Mutant::CLI#add_debug_options
- Mutant::CLI#add_environment_options
- Mutant::Isolation::Fork#self.call
- Mutant::Integration::Minitest#call
- Mutant::Reporter::CLI::Printer::Config#run
- Mutant::Reporter::CLI::Printer::EnvProgress#run
- Mutant::Runner#run_driver
Expand Down Expand Up @@ -130,6 +131,7 @@ UtilityFunction:
- Mutant::Integration::Null#call
- Mutant::Integration::Rspec#parse_example
- Mutant::Integration::Rspec#parse_expression # intentional, private
- Mutant::Integration::Minitest#test_case # intentional, private
- Mutant::Meta::Example::Verification#format_mutation
- Mutant::Reporter::CLI::Format::Progressive#new_buffer
- Mutant::Reporter::CLI::Printer::StatusProgressive#object # False positive calls super
Expand Down
152 changes: 152 additions & 0 deletions lib/mutant/integration/minitest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
require 'minitest'

# monkey patch for minitest
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'.freeze
IDENTIFICATION_FORMAT = 'minitest:%s#%s'.freeze

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 [Array<String>]
def expression_syntax
klass.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
def call(tests)
test_cases = tests.map(&all_tests_index.method(:fetch)).to_set

output = StringIO.new
reporter = ::Minitest::SummaryReporter.new(output)
start = Time.now

passed = test_cases.all? { |test| test.call(reporter) }
output.rewind

Result::Test.new(
passed: 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.(test_case.expression_syntax)
)
end

# All minitest test cases
#
# Intentional utility method.
#
# @return [Array<TestCase>]
def all_test_cases
::Minitest::Runnable.runnables.flat_map(&method(:test_case))
end

# Turn a minitest runnable into its test cases
#
# Intentional utility method.
#
# @param [Object] runnable
#
# @return [Array<TestCase>]
def test_case(runnable)
runnable.runnable_methods.map(&TestCase.method(:new))
end

end # Minitest
end # Integration
end # Mutant
22 changes: 22 additions & 0 deletions mutant-minitest.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# encoding: utf-8
#
require File.expand_path('../lib/mutant/version', __FILE__)

Gem::Specification.new do |gem|
gem.name = 'mutant-minitest'
gem.version = Mutant::VERSION.dup
gem.authors = ['Markus Schirp']
gem.email = %w[[email protected]]
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/integration/minitest.rb`.split("\n")
gem.test_files = `git ls-files -- spec/integration/mutant/minitest.rb`.split("\n")
gem.extra_rdoc_files = %w[TODO LICENSE]

gem.add_runtime_dependency('mutant', "~> #{gem.version}")
gem.add_runtime_dependency('minitest', '~> 5.5')
end
10 changes: 10 additions & 0 deletions spec/integration/mutant/minitest_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
RSpec.describe 'minitest integration', mutant: false do

let(:base_cmd) { 'bundle exec mutant -I test -I lib --require test_app --use minitest' }

context 'Minitest 5.5.0' do
let(:gemfile) { 'Gemfile.minitest-stdlib' }

it_behaves_like 'framework integration'
end
end
3 changes: 2 additions & 1 deletion spec/shared/framework_integration_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
specify 'it allows to exclude mutations' do
cli = <<-CMD.split("\n").join(' ')
#{base_cmd}
--ignore-subject TestApp::Literal#uncovered_string
TestApp::Literal#string
TestApp::Literal#uncovered_string
--ignore-subject TestApp::Literal#uncovered_string
CMD

expect(Kernel.system(cli)).to be(true)
end

Expand Down
41 changes: 41 additions & 0 deletions spec/unit/mutant/integration/isolation_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
RSpec.describe Mutant::Isolation::Fork, mutant: false do
let(:object) { Mutant::Config::DEFAULT.isolation }

it 'does isolate side effects' do
initial = 1
object.call { initial = 2 }
expect(initial).to be(1)
end

it 'return block value' do
expect(object.call { :foo }).to be(:foo)
end

it 'wraps exceptions' do
expect { object.call { fail } }.to raise_error(
Mutant::Isolation::Error,
'marshal data too short'
)
end

it 'wraps exceptions caused by crashing ruby' do
expect do
object.call do
fail RbBug.call
end
end.to raise_error(Mutant::Isolation::Error)
end

it 'redirects $stderr of children to /dev/null' do
begin
Tempfile.open('mutant-test') do |file|
$stderr = file
object.call { $stderr.puts('test') }
file.rewind
expect(file.read).to eql('')
end
ensure
$stderr = STDERR
end
end
end
6 changes: 6 additions & 0 deletions test_app/Gemfile.minitest-stdlib
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source 'https://rubygems.org'

gem 'minitest', '~> 5.5'
gem 'mutant', path: '../'
gem 'mutant-minitest', path: '../'
gem 'adamantium'
19 changes: 19 additions & 0 deletions test_app/test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
$LOAD_PATH << File.join(File.dirname(__FILE__), '../lib')

require 'test_app'
require 'minitest/autorun'

# require spec support files and shared behavior
Dir[File.expand_path('../{support,shared}/**/*.rb', __FILE__)].sort.each do |file|
require file
end

class TestAppTest < Minitest::Test
def self.cover(expression)
@expression = expression
end

def self.cover_expression
@expression or fail "Cover expression for #{self} is not specified"
end
end
19 changes: 19 additions & 0 deletions test_app/test/unit/test_app/literal_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'test_helper'

class TestApp::LiteralTest < TestAppTest
cover 'TestApp::Literal*'

def test_command
object = ::TestApp::Literal.new
subject = object.command('x')

assert_equal object, subject
end

def test_string
object = ::TestApp::Literal.new
subject = object.string

assert_equal 'string', subject
end
end

0 comments on commit 11474b6

Please sign in to comment.