Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test minitest integration #445

Merged
merged 2 commits into from Nov 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion .circleci/config.yml
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion config/flay.yml
@@ -1,3 +1,3 @@
---
threshold: 16
total_score: 1364
total_score: 1395
174 changes: 174 additions & 0 deletions 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could rewrite this monkey patch on one line with something like

Minitest.define_singleton_method(:autorun) { }

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea to shorten it like this. I wounder how we can apply the axioms to deterministically prefer one form over the other. //cc @dkubb.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of partial to the def form myself. I generally prefer to use built-in language syntax over meta-programming methods, assuming the end result is equal.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

built-in language syntax over meta-programming methods, assuming the end result is equal.

Yes. And the rule of least power supports this as the metaprogramming closure has a much wider scope. Using the primary syntax has "less power" (scope wise) so should be preferred here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to stick with the primary syntax here you could still cut two lines by instead writing

def Minitest.autorun
end

I know this syntax is usually frowned upon but it might make sense in a context like this. It also has the benefit then of not pinning YARD documentation to the toplevel Minitest namespace.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reopening the scope Minitest is not needed technically, but maybe for consistency?


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
56 changes: 56 additions & 0 deletions 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)
22 changes: 22 additions & 0 deletions 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
4 changes: 2 additions & 2 deletions mutant.gemspec
Expand Up @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions 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
10 changes: 10 additions & 0 deletions spec/integrations.yml
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions spec/support/corpus.rb
Expand Up @@ -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')
Expand Down
6 changes: 6 additions & 0 deletions 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'
16 changes: 16 additions & 0 deletions 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