Class: Mutineer::Config

Inherits:
Struct
  • Object
show all
Defined in:
lib/mutineer/config.rb

Overview

Plain run configuration, populated by the CLI (or directly by the integration test). operators nil means "all default operators"; threshold 0.0 means the CI gate is off (spec §10).

M5 adds: jobs (parallel workers), format (human|json), output (report file), strategy (reload|redefine), require_paths (extra files to load). Config loading and the CLI > file > default precedence merge live here (KTD3/KTD4).

Boot mode adds: boot (a file to require ONCE in the parent so the app env — e.g. Rails — is booted before forking; sources are then NOT manually required) and rails (sugar: defaults boot to config/environment and strategy to redefine, and reconnects ActiveRecord per fork).

Constant Summary collapse

CONFIG_FILE =
".mutineer.yml"
KNOWN_KEYS =

Keys accepted in .mutineer.yml (R7). require maps to the :require_paths field.

%w[operators jobs threshold only require boot rails since framework verbose ignore baseline].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**kwargs) ⇒ Config

Returns a new instance of Config.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/mutineer/config.rb', line 35

def initialize(**kwargs)
  super
  self.sources       ||= []
  self.tests         ||= []
  self.threshold     ||= 0.0
  self.dry_run       ||= false
  self.cache_dir     ||= ".mutineer"
  self.project_root  ||= Dir.pwd
  self.load_paths    ||= ["lib"]
  self.jobs          ||= Etc.nprocessors
  self.format        ||= "human"
  self.strategy      ||= "reload"
  self.require_paths ||= []
  self.rails         = false if rails.nil?
  self.verbose       = false if verbose.nil?
  self.ignore        ||= []
  self.baseline_epsilon ||= 0.0
end

Instance Attribute Details

#baselineObject

Returns the value of attribute baseline

Returns:

  • (Object)

    the current value of baseline



23
24
25
# File 'lib/mutineer/config.rb', line 23

def baseline
  @baseline
end

#baseline_epsilonObject

Returns the value of attribute baseline_epsilon

Returns:

  • (Object)

    the current value of baseline_epsilon



23
24
25
# File 'lib/mutineer/config.rb', line 23

def baseline_epsilon
  @baseline_epsilon
end

#bootObject

Returns the value of attribute boot

Returns:

  • (Object)

    the current value of boot



23
24
25
# File 'lib/mutineer/config.rb', line 23

def boot
  @boot
end

#cache_dirObject

Returns the value of attribute cache_dir

Returns:

  • (Object)

    the current value of cache_dir



23
24
25
# File 'lib/mutineer/config.rb', line 23

def cache_dir
  @cache_dir
end

#dry_runObject

Returns the value of attribute dry_run

Returns:

  • (Object)

    the current value of dry_run



23
24
25
# File 'lib/mutineer/config.rb', line 23

def dry_run
  @dry_run
end

#formatObject

Returns the value of attribute format

Returns:

  • (Object)

    the current value of format



23
24
25
# File 'lib/mutineer/config.rb', line 23

def format
  @format
end

#frameworkObject

Returns the value of attribute framework

Returns:

  • (Object)

    the current value of framework



23
24
25
# File 'lib/mutineer/config.rb', line 23

def framework
  @framework
end

#ignoreObject

Returns the value of attribute ignore

Returns:

  • (Object)

    the current value of ignore



23
24
25
# File 'lib/mutineer/config.rb', line 23

def ignore
  @ignore
end

#jobsObject

Returns the value of attribute jobs

Returns:

  • (Object)

    the current value of jobs



23
24
25
# File 'lib/mutineer/config.rb', line 23

def jobs
  @jobs
end

#load_pathsObject

Returns the value of attribute load_paths

Returns:

  • (Object)

    the current value of load_paths



23
24
25
# File 'lib/mutineer/config.rb', line 23

def load_paths
  @load_paths
end

#onlyObject

Returns the value of attribute only

Returns:

  • (Object)

    the current value of only



23
24
25
# File 'lib/mutineer/config.rb', line 23

def only
  @only
end

#operatorsObject

Returns the value of attribute operators

Returns:

  • (Object)

    the current value of operators



23
24
25
# File 'lib/mutineer/config.rb', line 23

def operators
  @operators
end

#outputObject

Returns the value of attribute output

Returns:

  • (Object)

    the current value of output



23
24
25
# File 'lib/mutineer/config.rb', line 23

def output
  @output
end

#project_rootObject

Returns the value of attribute project_root

Returns:

  • (Object)

    the current value of project_root



23
24
25
# File 'lib/mutineer/config.rb', line 23

def project_root
  @project_root
end

#railsObject

Returns the value of attribute rails

Returns:

  • (Object)

    the current value of rails



23
24
25
# File 'lib/mutineer/config.rb', line 23

def rails
  @rails
end

#require_pathsObject

Returns the value of attribute require_paths

Returns:

  • (Object)

    the current value of require_paths



23
24
25
# File 'lib/mutineer/config.rb', line 23

def require_paths
  @require_paths
end

#sinceObject

Returns the value of attribute since

Returns:

  • (Object)

    the current value of since



23
24
25
# File 'lib/mutineer/config.rb', line 23

def since
  @since
end

#sourcesObject

Returns the value of attribute sources

Returns:

  • (Object)

    the current value of sources



23
24
25
# File 'lib/mutineer/config.rb', line 23

def sources
  @sources
end

#strategyObject

Returns the value of attribute strategy

Returns:

  • (Object)

    the current value of strategy



23
24
25
# File 'lib/mutineer/config.rb', line 23

def strategy
  @strategy
end

#testsObject

Returns the value of attribute tests

Returns:

  • (Object)

    the current value of tests



23
24
25
# File 'lib/mutineer/config.rb', line 23

def tests
  @tests
end

#thresholdObject

Returns the value of attribute threshold

Returns:

  • (Object)

    the current value of threshold



23
24
25
# File 'lib/mutineer/config.rb', line 23

def threshold
  @threshold
end

#verboseObject

Returns the value of attribute verbose

Returns:

  • (Object)

    the current value of verbose



23
24
25
# File 'lib/mutineer/config.rb', line 23

def verbose
  @verbose
end

Class Method Details

.coerce(known_key, value, file_name) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mutineer/config.rb', line 141

def self.coerce(known_key, value, file_name)
  case known_key
  when "operators" then filter_operators(Array(value).map(&:to_s), file_name)
  when "jobs"      then value.to_i
  when "threshold" then value.to_f
  when "require"   then Array(value).map(&:to_s)
  when "boot"      then value.to_s
  when "framework" then value.to_s
  when "rails"     then value == true || value.to_s == "true"
  when "verbose"   then value == true || value.to_s == "true"
  when "ignore"    then Array(value).map(&:to_s)
  when "baseline"  then value.to_s
  else value
  end
end

.detect_framework(tests) ⇒ Object

Pick rspec when a MAJORITY of the given test files end with _spec.rb; otherwise minitest. Empty/ambiguous -> minitest (the safe default).



131
132
133
134
135
# File 'lib/mutineer/config.rb', line 131

def self.detect_framework(tests)
  tests = Array(tests)
  specs = tests.count { |t| t.to_s.end_with?("_spec.rb") }
  specs > tests.length / 2.0 ? "rspec" : "minitest"
end

.field_for(known_key) ⇒ Object



137
138
139
# File 'lib/mutineer/config.rb', line 137

def self.field_for(known_key)
  known_key == "require" ? :require_paths : known_key.to_sym
end

.filter_operators(names, file_name) ⇒ Object

Drop (with a warning) operator names the registry doesn't know (R7). Referenced lazily so config.rb carries no load-order dependency on the registry; by the time a config is parsed at runtime, it is loaded.



160
161
162
163
164
165
166
167
168
169
# File 'lib/mutineer/config.rb', line 160

def self.filter_operators(names, file_name)
  known = MutatorRegistry::ALL.keys
  names.select do |n|
    next true if known.include?(n)

    warn "mutineer: unknown operator #{n.inspect} in #{file_name} " \
         "(known: #{known.join(', ')}); ignored"
    false
  end
end

.find_file(start = Dir.pwd, home = File.expand_path("~")) ⇒ Object

Walk from start toward home, returning the first .mutineer.yml path found or nil. Checks home itself, then stops; if start is above home (e.g. /tmp), the walk continues to the filesystem root (KTD4). Pure discovery — reads no file content.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/mutineer/config.rb', line 58

def self.find_file(start = Dir.pwd, home = File.expand_path("~"))
  dir = File.expand_path(start)
  loop do
    candidate = File.join(dir, CONFIG_FILE)
    return candidate if File.file?(candidate)
    break if dir == home

    parent = File.dirname(dir)
    break if parent == dir # filesystem root

    dir = parent
  end
  nil
end

.from_file(path) ⇒ Object

Parse a .mutineer.yml into a symbol-keyed hash of recognized keys. Unknown keys / unknown operator names emit a one-line stderr warning and are ignored (R7). A YAML syntax error raises ConfigError (R7a/R8) — never a silent fallback to defaults, and never an exit from the lib layer.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/mutineer/config.rb', line 77

def self.from_file(path)
  raw = YAML.safe_load(File.read(path)) || {}
  name = File.basename(path)
  unless raw.is_a?(Hash)
    warn "mutineer: #{name} ignored: expected a YAML mapping of keys to values"
    return {}
  end

  out = {}
  raw.each do |key, value|
    ks = key.to_s
    unless KNOWN_KEYS.include?(ks)
      warn "mutineer: unknown config key #{ks.inspect} in #{name} " \
           "(known: #{KNOWN_KEYS.join(', ')}); ignored"
      next
    end
    out[field_for(ks)] = coerce(ks, value, name)
  end
  out
rescue Psych::SyntaxError => e
  raise ConfigError, "#{File.basename(path)} parse error: #{e.message}"
end

.resolve(cli_opts, file_hash, explicit) ⇒ Object

Apply precedence (KTD3): start from the CLI-provided values, then fill in a config-file value only for keys the user did NOT type on the command line. explicit is a Set of field symbols the CLI saw with a value; a Set (not nil-sentinels) is used because some valid values are zero/false.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/mutineer/config.rb', line 104

def self.resolve(cli_opts, file_hash, explicit)
  merged = cli_opts.dup
  file_hash.each { |k, v| merged[k] = v unless explicit.include?(k) }
  config = new(**merged)

  # --rails sugar: boot config/environment and prefer the surgical (redefine)
  # strategy, which avoids writing tempfiles into the app tree and Zeitwerk
  # reload hazards. An explicit --strategy always wins.
  if config.rails
    config.boot ||= "config/environment"
    config.strategy = "redefine" unless explicit.include?(:strategy)
    # #12: parallel mutant forks share one database; transactional-fixture
    # setup/teardown across processes contends and deadlocks. Default to
    # serial under --rails; an explicit --jobs N opts back into parallelism
    # (with the per-worker DB-isolation that implies).
    config.jobs = 1 unless explicit.include?(:jobs)
  end

  # Auto-detect the framework only when neither CLI nor config file set it
  # (explicit value, from either source, is already on config.framework and
  # always wins). Default minitest unless the test files clearly look RSpec.
  config.framework ||= detect_framework(config.tests)
  config
end