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].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**kwargs) ⇒ Config

Returns a new instance of Config.



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

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?
end

Instance Attribute Details

#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

#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

Class Method Details

.coerce(known_key, value, file_name) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/mutineer/config.rb', line 137

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"
  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).



127
128
129
130
131
# File 'lib/mutineer/config.rb', line 127

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



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

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.



153
154
155
156
157
158
159
160
161
162
# File 'lib/mutineer/config.rb', line 153

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.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/mutineer/config.rb', line 54

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.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/mutineer/config.rb', line 73

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.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/mutineer/config.rb', line 100

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