free_mutant/lib/mutant/integration/rspec.rb

153 lines
4.3 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2018-09-12 13:15:43 +00:00
require 'rspec/core'
module Mutant
2014-06-28 23:04:18 +00:00
class Integration
2015-05-16 22:10:00 +00:00
# Rspec integration
#
# This looks so complicated, because rspec:
#
# * Keeps its state global in RSpec.world and lots of other places
# * There is no API to "just run a subset of examples", the examples
# need to be selected in-place via mutating the `RSpec.filtered_examples`
2015-11-18 23:06:22 +00:00
# data structure
2015-05-16 22:10:00 +00:00
# * Does not maintain a unique identification for an example,
# aside the instances of `RSpec::Core::Example` objects itself.
2015-07-15 14:04:45 +00:00
# For that reason identifying examples by:
2015-05-16 22:10:00 +00:00
# * full description
# * location
2015-07-15 14:04:45 +00:00
# Is NOT enough. It would not be unique. So we add an "example index"
2015-05-16 22:10:00 +00:00
# for unique reference.
#
# :reek:TooManyConstants
2014-06-28 23:04:18 +00:00
class Rspec < self
ALL_EXPRESSION = Expression::Namespace::Recursive.new(scope_name: nil)
2015-05-16 22:10:00 +00:00
EXPRESSION_CANDIDATE = /\A([^ ]+)(?: )?/.freeze
LOCATION_DELIMITER = ':'
EXIT_SUCCESS = 0
CLI_OPTIONS = IceNine.deep_freeze(%w[spec --fail-fast])
TEST_ID_FORMAT = 'rspec:%<index>d:%<location>s/%<description>s'
private_constant(*constants(false))
# Initialize rspec integration
#
# @return [undefined]
def initialize(*)
super
@output = StringIO.new
@runner = RSpec::Core::Runner.new(RSpec::Core::ConfigurationOptions.new(CLI_OPTIONS))
@world = RSpec.world
end
2014-06-28 23:04:18 +00:00
# Setup rspec integration
#
# @return [self]
def setup
@runner.setup($stderr, @output)
self
end
memoize :setup
# Run a collection of tests
#
# @param [Enumerable<Mutant::Test>] tests
#
2015-01-24 23:12:15 +00:00
# @return [Result::Test]
2014-06-28 20:52:47 +00:00
#
# rubocop:disable MethodLength
def call(tests)
2015-05-16 22:10:00 +00:00
examples = tests.map(&all_tests_index.method(:fetch))
filter_examples(&examples.method(:include?))
start = Timer.now
2015-05-16 22:10:00 +00:00
passed = @runner.run_specs(@world.ordered_example_groups).equal?(EXIT_SUCCESS)
@output.rewind
Result::Test.new(
output: @output.read,
passed: passed,
runtime: Timer.now - start,
tests: tests
)
end
# Available tests
#
# @return [Enumerable<Test>]
def all_tests
all_tests_index.keys
end
memoize :all_tests
private
# Index of available tests
#
# @return [Hash<Test, RSpec::Core::Example]
def all_tests_index
all_examples.each_with_index.each_with_object({}) do |(example, example_index), index|
index[parse_example(example, example_index)] = example
end
end
memoize :all_tests_index
# Parse example into test
#
# @param [RSpec::Core::Example] example
# @param [Integer] index
#
# @return [Test]
def parse_example(example, index)
metadata = example.metadata
id = TEST_ID_FORMAT % {
index: index,
location: metadata.fetch(:location),
description: metadata.fetch(:full_description)
}
2015-05-16 22:10:00 +00:00
Test.new(
expression: parse_expression(metadata),
id: id
)
end
# Parse metadata into expression
#
2015-11-18 23:06:22 +00:00
# @param [RSpec::Core::Example::MetaData] metadata
#
# @return [Expression]
def parse_expression(metadata)
if metadata.key?(:mutant_expression)
expression_parser.(metadata.fetch(:mutant_expression))
else
match = EXPRESSION_CANDIDATE.match(metadata.fetch(:full_description))
expression_parser.try_parse(match.captures.first) || ALL_EXPRESSION
end
end
# Available rspec examples
#
# @return [Array<String, RSpec::Core::Example]
def all_examples
@world.example_groups.flat_map(&:descendants).flat_map(&:examples).select do |example|
example.metadata.fetch(:mutant, true)
end
end
# Filter examples
#
# @param [#call] predicate
#
# @return [undefined]
def filter_examples(&predicate)
@world.filtered_examples.each_value do |examples|
examples.keep_if(&predicate)
end
end
2014-06-28 23:04:18 +00:00
end # Rspec
end # Integration
2013-06-14 18:54:02 +00:00
end # Mutant