Skip to content
Permalink
Browse files

Add config file support

  • Loading branch information
mbj committed Feb 20, 2019
1 parent c50c235 commit b03d69d21f738e7c64b312c7877c6768cd4434e3
@@ -11,10 +11,7 @@ namespace :metrics do
task mutant: :coverage do
arguments = %w[
bundle exec mutant
--include lib
--since HEAD~1
--require mutant
--use rspec
--zombie
]
arguments.concat(%w[--jobs 4]) if ENV.key?('CIRCLECI')
@@ -20,6 +20,7 @@
require 'singleton'
require 'stringio'
require 'unparser'
require 'yaml'

# This setting is done to make errors within the parallel
# reporter / execution visible in the main thread.
@@ -2,6 +2,8 @@

module Mutant
# Commandline parser / runner
#
# rubocop:disable Metrics/ClassLength
class CLI
include Concord.new(:world, :config)

@@ -22,7 +24,7 @@ class CLI
# @param [World] world
# the outside world
#
# @param [Config] config
# @param [Config] default_config
# the default config
#
# @param [Array<String>]
@@ -33,8 +35,9 @@ class CLI
# rubocop:disable Style/Semicolon
#
# ignore :reek:LongParameterList
def self.run(world, config, arguments)
apply(world, config, arguments)
def self.run(world, default_config, arguments)
Config.load_config_file(world, default_config)
.apply { |file_config| apply(world, file_config, arguments) }
.apply { |cli_config| Bootstrap.apply(world, cli_config) }
.fmap(&Runner.method(:call))
.from_right { |error| world.stderr.puts(error); return false }
@@ -209,6 +212,6 @@ def add(attribute, value)
def add_matcher(attribute, value)
with(matcher: config.matcher.add(attribute, value))
end

end # CLI
# rubocop:enable Metrics/ClassLength
end # Mutant
@@ -53,5 +53,73 @@ class Config
define_method(:"#{name}?") { public_send(name) }
end

boolean = Transform::Boolean.new
integer = Transform::Primitive.new(Integer)
string = Transform::Primitive.new(String)

string_array = Transform::Array.new(string)

TRANSFORM = Transform::Sequence.new(
[
Transform::Exception.new(SystemCallError, :read.to_proc),
Transform::Exception.new(YAML::SyntaxError, YAML.method(:load)),
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('fail_fast', boolean),
Transform::Hash::Key.new('includes', string_array),
Transform::Hash::Key.new('integration', string),
Transform::Hash::Key.new('jobs', integer),
Transform::Hash::Key.new('requires', string_array)
],
required: []
),
Transform::Hash::Symbolize.new
]
)

MORE_THAN_ONE_CONFIG_FILE = <<~'MESSAGE'
Found more than one candidate for use as implicit config file: %s
MESSAGE

CANDIDATES = %w[
.mutant.yml
config/mutant.yml
mutant.yml
].freeze

private_constant(*constants(false))

# Load config file
#
# @param [World] world
# @param [Config] config
#
# @return [Either<String,Config>]
def self.load_config_file(world, config)
files = CANDIDATES.map(&world.pathname.method(:new)).select(&:readable?)

if files.one?
load_contents(files.first).fmap(&config.method(:with))
elsif files.empty?
Either::Right.new(config)
else
Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
end
end

# Load contents of file
#
# @param [Pathname] path
#
# @return [Config]
#
# @raise [Either<String, Hash{Symbol => Object}>]
# in case of config file error
def self.load_contents(path)
Transform::Named
.new(path.to_s, TRANSFORM)
.apply(path).lmap(&:compact_message)
end
private_class_method :load_contents
end # Config
end # Mutant
@@ -487,5 +487,20 @@ def apply(input)
success(current)
end
end # Sequence

# Generic exception transformer
class Exception < self
include Concord.new(:error_class, :block)

# Apply transformation to input
#
# @param [Object]
#
# @return [Either<Error, Object>]
def apply(input)
Either.wrap_error(error_class) { block.call(input) }
.lmap { |exception| error(input: input, message: exception.to_s) }
end
end # Exception
end # Transform
end # Mutant
@@ -0,0 +1,6 @@
---
includes:
- lib
integration: rspec
requires:
- mutant
@@ -7,14 +7,6 @@
let(:stdout) { instance_double(IO, 'stdout', puts: undefined) }
let(:target_stream) { stdout }

let(:config) do
default_config.with(
fail_fast: false,
includes: [],
requires: []
)
end

let(:world) do
instance_double(
Mutant::World,
@@ -40,21 +32,24 @@

describe '.run' do
def apply
described_class.run(world, config, arguments)
described_class.run(world, default_config, arguments)
end

let(:arguments) { instance_double(Array) }
let(:env) { instance_double(Mutant::Env) }
let(:env_result) { Mutant::Either::Right.new(env) }
let(:report_success) { true }
let(:cli_result) { Mutant::Either::Right.new(new_config) }
let(:new_config) { instance_double(Mutant::Config, 'parsed config') }
let(:arguments) { instance_double(Array) }
let(:cli_config) { instance_double(Mutant::Config, 'cli config') }
let(:cli_result) { Mutant::Either::Right.new(cli_config) }
let(:env) { instance_double(Mutant::Env) }
let(:env_result) { Mutant::Either::Right.new(env) }
let(:file_config) { instance_double(Mutant::Config, 'file config') }
let(:file_result) { Mutant::Either::Right.new(file_config) }
let(:report_success) { true }

let(:report) do
instance_double(Mutant::Result::Env, success?: report_success)
end

before do
allow(Mutant::Config).to receive_messages(load_config_file: file_result)
allow(Mutant::CLI).to receive_messages(apply: cli_result)
allow(Mutant::Bootstrap).to receive_messages(apply: env_result)
allow(Mutant::Runner).to receive_messages(call: report)
@@ -63,14 +58,19 @@ def apply
it 'performs calls in expected sequence' do
apply

expect(Mutant::Config)
.to have_received(:load_config_file)
.with(world, default_config)
.ordered

expect(Mutant::CLI)
.to have_received(:apply)
.with(world, config, arguments)
.with(world, file_config, arguments)
.ordered

expect(Mutant::Bootstrap)
.to have_received(:apply)
.with(world, new_config)
.with(world, cli_config)
.ordered

expect(Mutant::Runner)
@@ -110,7 +110,7 @@ def apply

describe '.apply' do
def apply
described_class.apply(world, config, arguments)
described_class.apply(world, default_config, arguments)
end

shared_examples 'invalid arguments' do
@@ -0,0 +1,126 @@
# frozen_string_literal: true

RSpec.describe Mutant::Config do
describe '.load_config_file' do
def apply
described_class.load_config_file(world, config)
end

let(:config) { Mutant::Config::DEFAULT }
let(:world) { instance_double(Mutant::World, pathname: pathname) }

let(:pathname) do
paths = paths()
Class.new do
define_singleton_method(:new, &paths.method(:fetch))
end
end

let(:config_mutant_yml) do
instance_double(Pathname, 'config/mutant.yml', readable?: false)
end

let(:dot_mutant_yml) do
instance_double(Pathname, '.mutant.yml', readable?: false)
end

let(:mutant_yml) do
instance_double(Pathname, 'mutant.yml', readable?: false)
end

let(:paths) do
{
'.mutant.yml' => dot_mutant_yml,
'config/mutant.yml' => config_mutant_yml,
'mutant.yml' => mutant_yml
}
end

before do
allow(Pathname).to receive(:new, &paths.method(:fetch))
end

context 'when no path is readable' do
it 'returns original config' do
expect(apply).to eql(Mutant::Either::Right.new(config))
end
end

context 'when more than one path is readable' do
before do
[config_mutant_yml, mutant_yml].each do |path|
allow(path).to receive_messages(readable?: true)
end
end

let(:expected_message) do
<<~MESSAGE
Found more than one candidate for use as implicit config file: #{config_mutant_yml}, #{mutant_yml}
MESSAGE
end

it 'returns expected failure' do
expect(apply).to eql(Mutant::Either::Left.new(expected_message))
end
end

context 'with one readable path' do
let(:path_contents) do
<<~'YAML'
---
integration: rspec
YAML
end

before do
allow(mutant_yml).to receive_messages(
read: path_contents,
readable?: true,
to_s: 'mutant.yml'
)
end

context 'when file can be read' do
context 'when yaml contents can be transformed' do
it 'returns expected config' do
expect(apply)
.to eql(Mutant::Either::Right.new(config.with(integration: 'rspec')))
end
end

context 'when yaml contents cannot be transformed' do
let(:path_contents) do
<<~'YAML'
---
integration: true
YAML
end

# rubocop:disable Metrics/LineLength
let(:expected_message) do
'mutant.yml/Mutant::Transform::Sequence/2/Mutant::Transform::Hash/["integration"]/String: Expected: String but got: TrueClass'
end
# rubocop:enable Metrics/LineLength

it 'returns expected error' do
expect(apply) .to eql(Mutant::Either::Left.new(expected_message))
end
end
end

context 'when file cannot be read' do
before do
allow(mutant_yml).to receive(:read).and_raise(SystemCallError, 'some error')
end

let(:expected_message) do
'mutant.yml/Mutant::Transform::Sequence/0/Mutant::Transform::Exception: unknown error - some error'
end

it 'returns expected error' do
expect(apply) .to eql(Mutant::Either::Left.new(expected_message))
end
end
end
end
end

0 comments on commit b03d69d

Please sign in to comment.
You can’t perform that action at this time.