Move bootstrapping into Env::Bootstrap

* Do not mix concerns of domain with building an object graph concerning
  the domains execution environment
* Removes the amount of clutter in Env (mostly a cleanup for tracing
  where Env will grow a bit)
This commit is contained in:
Markus Schirp 2014-12-22 14:42:20 +00:00
parent 73474f312f
commit b65939d527
11 changed files with 316 additions and 203 deletions

View file

@ -5,3 +5,4 @@ source 'https://rubygems.org'
gemspec name: 'mutant'
gem 'devtools', git: 'https://github.com/rom-rb/devtools.git'
eval_gemfile 'Gemfile.devtools'

View file

@ -1,3 +1,3 @@
---
threshold: 18
total_score: 1198
total_score: 1217

View file

@ -95,6 +95,7 @@ end # Mutant
require 'mutant/version'
require 'mutant/env'
require 'mutant/env/bootstrap'
require 'mutant/ast'
require 'mutant/ast/sexp'
require 'mutant/ast/types'

View file

@ -22,7 +22,7 @@ module Mutant
# @api private
#
def self.run(arguments)
Runner.call(Env.new(call(arguments))).success? ? EXIT_SUCCESS : EXIT_FAILURE
Runner.call(Env::Bootstrap.call(call(arguments))).success? ? EXIT_SUCCESS : EXIT_FAILURE
rescue Error => exception
$stderr.puts(exception.message)
EXIT_FAILURE

View file

@ -1,39 +1,19 @@
module Mutant
# Abstract base class for mutant environments
class Env
include Adamantium::Flat, Concord::Public.new(:config, :cache)
include Adamantium::Flat, Anima::Update, Anima.new(
:config,
:actor_env,
:cache,
:subjects,
:matchable_scopes,
:mutations
)
SEMANTICS_MESSAGE =
"Fix your lib to follow normal ruby semantics!\n" \
'{Module,Class}#name should return resolvable constant name as String or nil'.freeze
# Return new env
#
# @param [Config] config
#
# @return [Env]
#
# @api private
#
def self.new(config, cache = Cache.new)
super(config, cache)
end
# Initialize env
#
# @return [undefined]
#
# @api private
#
def initialize(*)
super
infect
initialize_matchable_scopes
initialize_subjects
initialize_mutations
end
# Print warning message
#
# @param [String]
@ -47,30 +27,6 @@ module Mutant
self
end
# Return subjects
#
# @return [Array<Subject>]
#
# @api private
#
attr_reader :subjects
# Return mutations
#
# @return [Array<Mutation>]
#
# @api private
#
attr_reader :mutations
# Return all usable match scopes
#
# @return [Array<Matcher::Scope>]
#
# @api private
#
attr_reader :matchable_scopes
# Kill mutation
#
# @param [Mutation] mutation
@ -87,95 +43,5 @@ module Mutant
)
end
private
# Return scope name
#
# @param [Class, Module] scope
#
# @return [String]
# if scope has a name and does not raise exceptions obtaining it
#
# @return [nil]
# otherwise
#
# @api private
#
# rubocop:disable LineLength
#
def scope_name(scope)
scope.name
rescue => exception
warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect}. #{SEMANTICS_MESSAGE}")
nil
end
# Try to turn scope into expression
#
# @param [Class, Module] scope
#
# @return [Expression]
# if scope can be represented in an expression
#
# @return [nil]
# otherwise
#
# @api private
#
def expression(scope)
name = scope_name(scope) or return
unless name.instance_of?(String)
warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}")
return
end
Expression.try_parse(name)
end
# Initialize subjects
#
# @return [undefined]
#
# @api private
#
def initialize_subjects
@subjects = Matcher::Compiler.call(self, config.matcher_config).to_a
end
# Initialize mutations
#
# @return [undefined]
#
# @api private
#
def initialize_mutations
@mutations = subjects.flat_map(&:mutations)
end
# Infect environment
#
# @return [undefined]
#
# @api private
#
def infect
config.includes.each(&$LOAD_PATH.method(:<<))
config.requires.each(&method(:require))
end
# Initialize matchable scopes
#
# @return [undefined]
#
# @api private
#
def initialize_matchable_scopes
@matchable_scopes = ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
expression = expression(scope)
aggregate << Matcher::Scope.new(self, scope, expression) if expression
end.sort_by(&:identification)
end
end # Env
end # Mutant

154
lib/mutant/env/bootstrap.rb vendored Normal file
View file

@ -0,0 +1,154 @@
module Mutant
class Env
# Boostrap environment
class Bootstrap
include Adamantium::Flat, Concord::Public.new(:config, :cache), Procto.call(:env)
SEMANTICS_MESSAGE =
"Fix your lib to follow normal ruby semantics!\n" \
'{Module,Class}#name should return resolvable constant name as String or nil'.freeze
# Return scopes that are eligible for mnatching
#
# @return [Enumerable<Matcher::Scope>]
#
# @api private
#
attr_reader :matchable_scopes
# Return new boostrap env
#
# @return [Env]
#
# @api private
#
def self.new(_config, _cache = Cache.new)
super
end
# Initialize object
#
# @return [Object]
#
# @api private
#
def initialize(*)
super
infect
initialize_matchable_scopes
end
# Print warning message
#
# @param [String]
#
# @return [self]
#
# @api private
#
def warn(message)
config.reporter.warn(message)
self
end
# Return environment after boostraping
#
# @return [Env]
#
# @api private
#
def env
subjects = matched_subjects
Env.new(
actor_env: Actor::Env.new(Thread),
config: config,
cache: cache,
subjects: subjects,
matchable_scopes: matchable_scopes,
mutations: subjects.flat_map(&:mutations)
)
end
private
# Return scope name
#
# @param [Class, Module] scope
#
# @return [String]
# if scope has a name and does not raise exceptions obtaining it
#
# @return [nil]
# otherwise
#
# @api private
#
# rubocop:disable LineLength
#
def scope_name(scope)
scope.name
rescue => exception
warn("#{scope.class}#name from: #{scope.inspect} raised an error: #{exception.inspect}. #{SEMANTICS_MESSAGE}")
nil
end
# Infect environment
#
# @return [undefined]
#
# @api private
#
def infect
config.includes.each(&$LOAD_PATH.method(:<<))
config.requires.each(&method(:require))
end
# Try to turn scope into expression
#
# @param [Class, Module] scope
#
# @return [Expression]
# if scope can be represented in an expression
#
# @return [nil]
# otherwise
#
# @api private
#
def expression(scope)
name = scope_name(scope) or return
unless name.instance_of?(String)
warn("#{scope.class}#name from: #{scope.inspect} returned #{name.inspect}. #{SEMANTICS_MESSAGE}")
return
end
Expression.try_parse(name)
end
# Return matched subjects
#
# @return [Enumerable<Subject>]
#
# @api private
#
def matched_subjects
Matcher::Compiler.call(self, config.matcher_config).to_a
end
# Initialize matchable scopes
#
# @return [undefined]
#
# @api private
#
def initialize_matchable_scopes
@matchable_scopes = ObjectSpace.each_object(Module).each_with_object([]) do |scope, aggregate|
expression = expression(scope)
aggregate << Matcher::Scope.new(self, scope, expression) if expression
end.sort_by(&:identification)
end
end # Boostrap
end # Env
end # Mutant

View file

@ -37,14 +37,14 @@ module Mutant
# Return matcher
#
# @param [Env] env
# @param [Env::Bootstrap] bootstrap
#
# @return [Matcher]
#
# @api private
#
def matcher(env)
Matcher::Namespace.new(env, self)
def matcher(bootstrap)
Matcher::Namespace.new(bootstrap, self)
end
# Return length of match
@ -74,14 +74,14 @@ module Mutant
# Return matcher
#
# @param [Cache] env
# @param [Env::Bootstrap] boostrap
#
# @return [Matcher]
#
# @api private
#
def matcher(env)
Matcher::Scope.new(env, Mutant.constant_lookup(namespace), self)
def matcher(bootstrap)
Matcher::Scope.new(bootstrap, Mutant.constant_lookup(namespace), self)
end
end # Exact

View file

@ -34,7 +34,7 @@ require 'test_app'
module Fixtures
TEST_CONFIG = Mutant::Config::DEFAULT.update(reporter: Mutant::Reporter::Trace.new)
TEST_CACHE = Mutant::Cache.new
TEST_ENV = Mutant::Env.new(TEST_CONFIG, TEST_CACHE)
TEST_ENV = Mutant::Env::Bootstrap.call(TEST_CONFIG, TEST_CACHE)
end # Fixtures
module ParserHelper

View file

@ -25,7 +25,7 @@ RSpec.describe Mutant::CLI do
before do
expect(Mutant::CLI).to receive(:call).with(arguments).and_return(config)
expect(Mutant::Env).to receive(:new).with(config).and_return(env)
expect(Mutant::Env::Bootstrap).to receive(:call).with(config).and_return(env)
expect(Mutant::Runner).to receive(:call).with(env).and_return(report)
end

129
spec/unit/mutant/env/boostrap_spec.rb vendored Normal file
View file

@ -0,0 +1,129 @@
RSpec.describe Mutant::Env::Bootstrap do
let(:config) do
Mutant::Config::DEFAULT.update(
jobs: 1,
reporter: Mutant::Reporter::Trace.new,
includes: [],
requires: [],
matcher_config: Mutant::Matcher::Config::DEFAULT
)
end
let(:expected_env) do
Mutant::Env.new(
cache: Mutant::Cache.new,
subjects: [],
matchable_scopes: [],
mutations: [],
config: config,
actor_env: Mutant::Actor::Env.new(Thread)
)
end
shared_examples_for 'bootstrap call' do
it { should eql(expected_env) }
end
let(:object_space_modules) { [] }
before do
allow(ObjectSpace).to receive(:each_object).with(Module).and_return(object_space_modules.each)
end
describe '.call' do
subject { described_class.call(config) }
context 'when Module#name calls result in exceptions' do
let(:invalid_class) do
Class.new do
def self.name
fail
end
end
end
let(:object_space_modules) { [invalid_class] }
after do
# Fix Class#name so other specs do not see this one
class << invalid_class
undef :name
def name
end
end
end
it 'warns via reporter' do
expected_warnings = [
"Class#name from: #{invalid_class} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"
]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
end
include_examples 'bootstrap call'
end
context 'when includes are present' do
let(:config) { super().update(includes: %w[foo bar]) }
before do
%w[foo bar].each do |component|
expect($LOAD_PATH).to receive(:<<).with(component).and_return($LOAD_PATH)
end
end
include_examples 'bootstrap call'
end
context 'when Module#name does not return a String or nil' do
let(:invalid_class) do
Class.new do
def self.name
Object
end
end
end
let(:object_space_modules) { [invalid_class] }
after do
# Fix Class#name so other specs do not see this one
class << invalid_class
undef :name
def name
end
end
end
it 'warns via reporter' do
expected_warnings = [
"Class#name from: #{invalid_class.inspect} returned Object. #{Mutant::Env::SEMANTICS_MESSAGE}"
]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
end
include_examples 'bootstrap call'
end
context 'when scope matches expression' do
let(:mutations) { [double('Mutation')] }
let(:subjects) { [double('Subject', mutations: mutations)] }
before do
expect(Mutant::Matcher::Compiler).to receive(:call).and_return(subjects)
end
let(:expected_env) do
super().update(
subjects: subjects,
mutations: mutations
)
end
include_examples 'bootstrap call'
end
end
end

View file

@ -1,58 +1,20 @@
RSpec.describe Mutant::Env do
let(:config) { Mutant::Config::DEFAULT.update(jobs: 1, reporter: Mutant::Reporter::Trace.new) }
context '.new' do
subject { described_class.new(config) }
context 'when Module#name calls result in exceptions' do
it 'warns via reporter' do
klass = Class.new do
def self.name
fail
end
end
expected_warnings = [
"Class#name from: #{klass} raised an error: RuntimeError. #{Mutant::Env::SEMANTICS_MESSAGE}"
]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
# Fix Class#name so other specs do not see this one
class << klass
undef :name
def name
end
end
end
end
context 'when Module#name does not return a String or nil' do
it 'warns via reporter' do
klass = Class.new do
def self.name
Object
end
end
expected_warnings = ["Class#name from: #{klass.inspect} returned Object. #{Mutant::Env::SEMANTICS_MESSAGE}"]
expect { subject }.to change { config.reporter.warn_calls }.from([]).to(expected_warnings)
# Fix Class#name so other specs do not see this one
class << klass
undef :name
def name
end
end
end
end
let(:object) do
described_class.new(
config: config,
actor_env: Mutant::Actor::Env.new(Thread),
cache: Mutant::Cache.new,
subjects: [],
mutations: [],
matchable_scopes: []
)
end
let(:config) { Mutant::Config::DEFAULT.update(jobs: 1, reporter: Mutant::Reporter::Trace.new) }
context '#kill_mutation' do
let(:object) { described_class.new(config) }
let(:result) { double('Result') }
let(:mutation) { double('Mutation') }
let(:result) { double('Result') }
let(:mutation) { double('Mutation') }
subject { object.kill_mutation(mutation) }