Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add environment variable configuration #1274

Merged
merged 1 commit into from
Nov 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# v.0.11.1 2021-11-xx

* [#1274](https://github.com/mbj/mutant/pull/1274)

Add ability to set environment variables via the CLI.
The environment variables are inserted before the application is
loaded. This is especially useful to load rails in test mode via setting
`RAILS_ENV=test`.

For CLI use the `--env` argument.
For config file use the `environment_variables` key.

# v.0.11.0 2021-10-18

* [#1270](https://github.com/mbj/mutant/pull/1270)
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ requires:

Additional requires can be added by providing the `-r` or `--require` option to the CLI.

#### `environment_variables`

Allows to set environment variables that are loaded just before the target application is
loaded. This is especially useful when dealing with rails that has to be initialized with
`RAILS_ENV=test` to behave correctly under mutation testing.

```yml
---
environment_variables:
RAILS_ENV: test
```

Additional environment variables can be added by providing the `--env KEY=VALUE` option to the CLI.

#### `integration`

Specifies which mutant integration to use. If your tests are writen in [RSpec](https://rspec.info/), this should be set to `rspec`. If your tests are written in [minitest](https://github.com/seattlerb/minitest), this should be set to `minitest`.
Expand Down
62 changes: 32 additions & 30 deletions lib/mutant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,46 +234,48 @@ module Mutant

module Mutant
WORLD = World.new(
condition_variable: ConditionVariable,
gem: Gem,
gem_method: method(:gem),
io: IO,
json: JSON,
kernel: Kernel,
load_path: $LOAD_PATH,
marshal: Marshal,
mutex: Mutex,
object_space: ObjectSpace,
open3: Open3,
pathname: Pathname,
process: Process,
stderr: $stderr,
stdout: $stdout,
thread: Thread,
timer: Timer.new(Process)
condition_variable: ConditionVariable,
environment_variables: ENV,
gem: Gem,
gem_method: method(:gem),
io: IO,
json: JSON,
kernel: Kernel,
load_path: $LOAD_PATH,
marshal: Marshal,
mutex: Mutex,
object_space: ObjectSpace,
open3: Open3,
pathname: Pathname,
process: Process,
stderr: $stderr,
stdout: $stdout,
thread: Thread,
timer: Timer.new(Process)
)

# Reopen class to initialize constant to avoid dep circle
class Config
DEFAULT = new(
coverage_criteria: Config::CoverageCriteria::EMPTY,
expression_parser: Expression::Parser.new([
coverage_criteria: Config::CoverageCriteria::EMPTY,
expression_parser: Expression::Parser.new([
Expression::Method,
Expression::Methods,
Expression::Namespace::Exact,
Expression::Namespace::Recursive
]),
fail_fast: false,
hooks: EMPTY_ARRAY,
includes: EMPTY_ARRAY,
integration: nil,
isolation: Mutant::Isolation::Fork.new(WORLD),
jobs: nil,
matcher: Matcher::Config::DEFAULT,
mutation_timeout: nil,
reporter: Reporter::CLI.build(WORLD.stdout),
requires: EMPTY_ARRAY,
zombie: false
fail_fast: false,
environment_variables: EMPTY_HASH,
hooks: EMPTY_ARRAY,
includes: EMPTY_ARRAY,
integration: nil,
isolation: Mutant::Isolation::Fork.new(WORLD),
jobs: nil,
matcher: Matcher::Config::DEFAULT,
mutation_timeout: nil,
reporter: Reporter::CLI.build(WORLD.stdout),
requires: EMPTY_ARRAY,
zombie: false
)
end # Config

Expand Down
4 changes: 4 additions & 0 deletions lib/mutant/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def self.infect(env)

hooks.run(:env_infection_pre, env)

config.environment_variables.each do |key, value|
world.environment_variables[key] = value
end

config.includes.each(&world.load_path.public_method(:<<))
config.requires.each(&world.kernel.public_method(:require))

Expand Down
10 changes: 10 additions & 0 deletions lib/mutant/cli/command/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Environment < self
add_matcher_options
].freeze

ENV_PATTERN = /\A(?<key>[A-Za-z\d]+)=(?<value>.*)\z/.freeze

private

def initialize(attributes)
Expand Down Expand Up @@ -62,6 +64,7 @@ def add_matcher(attribute, value)
set(matcher: @config.matcher.add(attribute, value))
end

# rubocop:disable Metrics/MethodLength
def add_environment_options(parser)
parser.separator('Environment:')
parser.on('--zombie', 'Run mutant zombified') do
Expand All @@ -73,7 +76,14 @@ def add_environment_options(parser)
parser.on('-r', '--require NAME', 'Require file with NAME') do |name|
add(:requires, name)
end
parser.on('--env KEY=VALUE', 'Set environment variable') do |value|
match = ENV_PATTERN.match(value) || fail("Invalid env variable: #{value.inspect}")
set(
environment_variables: @config.environment_variables.merge(match[:key] => match[:value])
)
end
end
# rubocop:enable Metrics/MethodLength

def add_integration_options(parser)
parser.separator('Integration:')
Expand Down
68 changes: 50 additions & 18 deletions lib/mutant/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ module Mutant
#
# Does not reference any "external" volatile state. The configuration applied
# to current environment is being represented by the Mutant::Env object.
#
# rubocop:disable Metrics/ClassLength
class Config
include Adamantium, Anima.new(
:coverage_criteria,
:environment_variables,
:expression_parser,
:fail_fast,
:hooks,
Expand Down Expand Up @@ -48,16 +51,17 @@ class Config
# rubocop:disable Metrics/MethodLength
def merge(other)
other.with(
coverage_criteria: coverage_criteria.merge(other.coverage_criteria),
fail_fast: fail_fast || other.fail_fast,
hooks: hooks + other.hooks,
includes: includes + other.includes,
integration: other.integration || integration,
jobs: other.jobs || jobs,
matcher: matcher.merge(other.matcher),
mutation_timeout: other.mutation_timeout || mutation_timeout,
requires: requires + other.requires,
zombie: zombie || other.zombie
coverage_criteria: coverage_criteria.merge(other.coverage_criteria),
environment_variables: environment_variables.merge(other.environment_variables),
fail_fast: fail_fast || other.fail_fast,
hooks: hooks + other.hooks,
includes: includes + other.includes,
integration: other.integration || integration,
jobs: other.jobs || jobs,
matcher: matcher.merge(other.matcher),
mutation_timeout: other.mutation_timeout || mutation_timeout,
requires: requires + other.requires,
zombie: zombie || other.zombie
)
end
# rubocop:enable Metrics/AbcSize
Expand Down Expand Up @@ -117,21 +121,48 @@ def self.env
)
)

# Parse a hash of environment variables
#
# @param [Hash<Object,Object>]
#
# @return [Either<String,Hash<String,String>]
#
def self.parse_environment_variables(hash)
invalid = hash.keys.reject { |key| key.instance_of?(String) }
return Either::Left.new("Non string keys: #{invalid}") if invalid.any?

invalid = hash.keys.grep_v(/\A[A-Za-z\d]+\z/)
return Either::Left.new("Invalid keys: #{invalid}") if invalid.any?

invalid = hash.values.reject { |value| value.instance_of?(String) }
return Either::Left.new("Non string values: #{invalid}") if invalid.any?

Either::Right.new(hash)
end
TRANSFORM = Transform::Sequence.new(
[
Transform::Exception.new(SystemCallError, :read.to_proc),
Transform::Exception.new(YAML::SyntaxError, YAML.public_method(:safe_load)),
Transform::Hash.new(
optional: [
Transform::Hash::Key.new('coverage_criteria', ->(value) { CoverageCriteria::TRANSFORM.call(value) }),
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
Transform::Hash::Key.new('integration', Transform::STRING),
Transform::Hash::Key.new('jobs', Transform::INTEGER),
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
Transform::Hash::Key.new(
'environment_variables',
Transform::Sequence.new(
[
Transform::Primitive.new(Hash),
Transform::Block.capture(:environment_variables, &method(:parse_environment_variables))
]
)
),
Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
Transform::Hash::Key.new('hooks', PATHNAME_ARRAY),
Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
Transform::Hash::Key.new('integration', Transform::STRING),
Transform::Hash::Key.new('jobs', Transform::INTEGER),
Transform::Hash::Key.new('matcher', Matcher::Config::LOADER),
Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
],
required: []
),
Expand All @@ -141,4 +172,5 @@ def self.env

private_constant(:TRANSFORM)
end # Config
# rubocop:enable Metrics/ClassLength
end # Mutant
1 change: 1 addition & 0 deletions lib/mutant/world.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Mutant
class World
include Adamantium, Anima.new(
:condition_variable,
:environment_variables,
:gem,
:gem_method,
:io,
Expand Down
35 changes: 18 additions & 17 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,24 @@ def undefined
# rubocop:disable Metrics/MethodLength
def fake_world
Mutant::World.new(
condition_variable: class_double(ConditionVariable),
gem: class_double(Gem),
gem_method: instance_double(Proc),
io: class_double(IO),
json: class_double(JSON),
kernel: class_double(Kernel),
load_path: instance_double(Array),
marshal: class_double(Marshal),
mutex: class_double(Mutex),
object_space: class_double(ObjectSpace),
open3: class_double(Open3),
pathname: class_double(Pathname),
process: class_double(Process),
stderr: instance_double(IO),
stdout: instance_double(IO),
thread: class_double(Thread),
timer: instance_double(Mutant::Timer)
condition_variable: class_double(ConditionVariable),
environment_variables: instance_double(Hash),
gem: class_double(Gem),
gem_method: instance_double(Proc),
io: class_double(IO),
json: class_double(JSON),
kernel: class_double(Kernel),
load_path: instance_double(Array),
marshal: class_double(Marshal),
mutex: class_double(Mutex),
object_space: class_double(ObjectSpace),
open3: class_double(Open3),
pathname: class_double(Pathname),
process: class_double(Process),
stderr: instance_double(IO),
stdout: instance_double(IO),
thread: class_double(Thread),
timer: instance_double(Mutant::Timer)
)
end
# rubocop:enable Metrics/MethodLength
Expand Down
29 changes: 18 additions & 11 deletions spec/unit/mutant/bootstrap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ def require(_); end

let(:config) do
Mutant::Config::DEFAULT.with(
includes: %w[include-a include-b],
integration: integration,
jobs: 1,
matcher: matcher_config,
reporter: instance_double(Mutant::Reporter),
requires: %w[require-a require-b]
environment_variables: { 'foo' => 'bar' },
includes: %w[include-a include-b],
integration: integration,
jobs: 1,
matcher: matcher_config,
reporter: instance_double(Mutant::Reporter),
requires: %w[require-a require-b]
)
end

Expand All @@ -52,11 +53,12 @@ def require(_); end
let(:world) do
instance_double(
Mutant::World,
kernel: kernel,
load_path: load_path,
object_space: object_space,
pathname: Pathname,
timer: timer
environment_variables: {},
kernel: kernel,
load_path: load_path,
object_space: object_space,
pathname: Pathname,
timer: timer
)
end

Expand All @@ -81,6 +83,11 @@ def require(_); end
selector: :run,
arguments: [:env_infection_pre, env_initial]
},
{
receiver: world.environment_variables,
selector: :[]=,
arguments: %w[foo bar]
},
{
receiver: load_path,
selector: :<<,
Expand Down
23 changes: 23 additions & 0 deletions spec/unit/mutant/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def self.main_body
--zombie Run mutant zombified
-I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH
-r, --require NAME Require file with NAME
--env KEY=VALUE Set environment variable


Runner:
Expand Down Expand Up @@ -965,6 +966,28 @@ def self.main_body
include_examples 'CLI run'
end

context 'with --env option' do
let(:arguments) { super() + %W[--env #{argument}] }

context 'on valid env syntax' do
let(:argument) { 'foo=bar' }

let(:bootstrap_config) do
super().with(environment_variables: { 'foo' => 'bar' })
end

include_examples 'CLI run'
end

context 'on invalid env syntax' do
let(:argument) { 'foobar' }

it 'raises expected error' do
expect { apply }.to raise_error(RuntimeError, 'Invalid env variable: "foobar"')
end
end
end

context 'with --jobs option on absent file config' do
let(:arguments) { super() + %w[--jobs 10] }
let(:bootstrap_config) { super().with(jobs: 10) }
Expand Down
Loading