require 'rspec/core' module Mutant class Integration # 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` # datastructure # * Does not maintain a unique identification for an example, # aside the instances of `RSpec::Core::Example` objects itself. # For that reason identifying examples by: # * full description # * location # Is NOT enough. It would not be unique. So we add an "example index" # for unique reference. class Rspec < self ALL_EXPRESSION = Expression::Namespace::Recursive.new(scope_name: nil) EXPRESSION_CANDIDATE = /\A([^ ]+)(?: )?/.freeze LOCATION_DELIMITER = ':'.freeze EXIT_SUCCESS = 0 CLI_OPTIONS = IceNine.deep_freeze(%w[spec --fail-fast]) private_constant(*constants(false)) register 'rspec' # Initialize rspec integration # # @return [undefined] # # @api private def initialize(*) super @output = StringIO.new @runner = RSpec::Core::Runner.new(RSpec::Core::ConfigurationOptions.new(CLI_OPTIONS)) @world = RSpec.world end # Setup rspec integration # # @return [self] # # @api private def setup @runner.setup($stderr, @output) self end memoize :setup # Run a collection of tests # # @param [Enumerable] tests # # @return [Result::Test] # # rubocop:disable MethodLength # # @api private def call(tests) examples = tests.map(&all_tests_index.method(:fetch)) filter_examples(&examples.method(:include?)) start = Time.now passed = @runner.run_specs(@world.ordered_example_groups).equal?(EXIT_SUCCESS) @output.rewind Result::Test.new( tests: tests, output: @output.read, runtime: Time.now - start, passed: passed ) end # Available tests # # @return [Enumerable] # # @api private def all_tests all_tests_index.keys end memoize :all_tests private # Index of available tests # # @return [Hash