From b65939d527037ddfd43b1aa080395c68d6946ff6 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 22 Dec 2014 14:42:20 +0000 Subject: [PATCH] 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) --- Gemfile | 1 + config/flay.yml | 2 +- lib/mutant.rb | 1 + lib/mutant/cli.rb | 2 +- lib/mutant/env.rb | 150 ++----------------------- lib/mutant/env/bootstrap.rb | 154 ++++++++++++++++++++++++++ lib/mutant/expression/namespace.rb | 12 +- spec/spec_helper.rb | 2 +- spec/unit/mutant/cli_spec.rb | 2 +- spec/unit/mutant/env/boostrap_spec.rb | 129 +++++++++++++++++++++ spec/unit/mutant/env_spec.rb | 64 +++-------- 11 files changed, 316 insertions(+), 203 deletions(-) create mode 100644 lib/mutant/env/bootstrap.rb create mode 100644 spec/unit/mutant/env/boostrap_spec.rb diff --git a/Gemfile b/Gemfile index cd316552..386405d5 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/config/flay.yml b/config/flay.yml index 6f7b4ad8..eb57601f 100644 --- a/config/flay.yml +++ b/config/flay.yml @@ -1,3 +1,3 @@ --- threshold: 18 -total_score: 1198 +total_score: 1217 diff --git a/lib/mutant.rb b/lib/mutant.rb index b4cc9c37..c150ddc5 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -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' diff --git a/lib/mutant/cli.rb b/lib/mutant/cli.rb index e0a4c106..3aafef49 100644 --- a/lib/mutant/cli.rb +++ b/lib/mutant/cli.rb @@ -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 diff --git a/lib/mutant/env.rb b/lib/mutant/env.rb index 3adda8bc..da39fcef 100644 --- a/lib/mutant/env.rb +++ b/lib/mutant/env.rb @@ -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] - # - # @api private - # - attr_reader :subjects - - # Return mutations - # - # @return [Array] - # - # @api private - # - attr_reader :mutations - - # Return all usable match scopes - # - # @return [Array] - # - # @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 diff --git a/lib/mutant/env/bootstrap.rb b/lib/mutant/env/bootstrap.rb new file mode 100644 index 00000000..b386b0e3 --- /dev/null +++ b/lib/mutant/env/bootstrap.rb @@ -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] + # + # @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] + # + # @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 diff --git a/lib/mutant/expression/namespace.rb b/lib/mutant/expression/namespace.rb index a79ee839..e16586e8 100644 --- a/lib/mutant/expression/namespace.rb +++ b/lib/mutant/expression/namespace.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ef2973b4..ee165d03 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 3ba42d53..d835a6ce 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -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 diff --git a/spec/unit/mutant/env/boostrap_spec.rb b/spec/unit/mutant/env/boostrap_spec.rb new file mode 100644 index 00000000..bdf5b846 --- /dev/null +++ b/spec/unit/mutant/env/boostrap_spec.rb @@ -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 diff --git a/spec/unit/mutant/env_spec.rb b/spec/unit/mutant/env_spec.rb index 0fce289a..7ce0e6be 100644 --- a/spec/unit/mutant/env_spec.rb +++ b/spec/unit/mutant/env_spec.rb @@ -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) }