Class: Mutineer::Config
- Inherits:
-
Struct
- Object
- Struct
- Mutineer::Config
- 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).
Also holds: 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; each layer holds only the keys the user wrote, and Config#explicit? reports them.
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, prefers redefine without a daemon, and reconnects ActiveRecord per fork).
Constant Summary collapse
- CONFIG_FILE =
Config file name.
".mutineer.yml"- KNOWN_KEYS =
Keys accepted in .mutineer.yml, derived from the schema.
requiremaps to the :require_paths field. CONFIG_OPTIONS.filter_map(&:yaml_key).freeze
Instance Attribute Summary collapse
-
#baseline ⇒ Object
Returns the value of attribute baseline.
-
#baseline_epsilon ⇒ Object
Returns the value of attribute baseline_epsilon.
-
#boot ⇒ Object
Returns the value of attribute boot.
-
#cache_dir ⇒ Object
Returns the value of attribute cache_dir.
-
#daemon ⇒ Object
Returns the value of attribute daemon.
-
#daemon_timeout ⇒ Object
Returns the value of attribute daemon_timeout.
-
#dry_run ⇒ Object
Returns the value of attribute dry_run.
-
#fail_fast ⇒ Object
Returns the value of attribute fail_fast.
-
#format ⇒ Object
Returns the value of attribute format.
-
#framework ⇒ Object
Returns the value of attribute framework.
-
#ignore ⇒ Object
Returns the value of attribute ignore.
-
#jobs ⇒ Object
Returns the value of attribute jobs.
-
#load_paths ⇒ Object
Returns the value of attribute load_paths.
-
#only ⇒ Object
Returns the value of attribute only.
-
#operators ⇒ Object
Returns the value of attribute operators.
-
#output ⇒ Object
Returns the value of attribute output.
-
#project_root ⇒ Object
Returns the value of attribute project_root.
-
#rails ⇒ Object
Returns the value of attribute rails.
-
#require_paths ⇒ Object
Returns the value of attribute require_paths.
-
#since ⇒ Object
Returns the value of attribute since.
-
#sources ⇒ Object
Returns the value of attribute sources.
-
#strategy ⇒ Object
Returns the value of attribute strategy.
-
#test_command ⇒ Object
Returns the value of attribute test_command.
-
#tests ⇒ Object
Returns the value of attribute tests.
-
#threshold ⇒ Object
Returns the value of attribute threshold.
-
#verbose ⇒ Object
Returns the value of attribute verbose.
Class Method Summary collapse
-
.detect_framework(tests) ⇒ String
Pick rspec when a MAJORITY of the given test files end with _spec.rb; otherwise minitest.
-
.field_for(known_key) ⇒ Symbol
Maps a config key to its Struct field.
-
.filter_operators(names, file_name) ⇒ Array<String>
private
Drop (with a warning) operator names the registry does not know.
-
.find_file(start = Dir.pwd, home = File.expand_path("~")) ⇒ Object
Walk from
starttowardhome, returning the first .mutineer.yml path found or nil. -
.finite_float(value) ⇒ Float?
private
Reads a finite Float from a number or a plain decimal string.
-
.from_file(path) ⇒ Object
Parse a .mutineer.yml into a symbol-keyed hash of recognized keys.
-
.parse(field, value, file: nil) ⇒ Object
Parses one raw value into the typed value for
field. -
.resolve(cli_opts, file_hash) ⇒ Mutineer::Config
Merges the two user layers, CLI over file, then derives what neither wrote.
Instance Method Summary collapse
-
#explicit?(key) ⇒ Boolean
True when the user wrote
key, on the command line or in the config file, whatever the value (falseandnilcount). -
#initialize(explicit: [], **kwargs) ⇒ Config
constructor
A new instance of Config.
Constructor Details
#initialize(explicit: [], **kwargs) ⇒ Config
Returns a new instance of Config.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/mutineer/config.rb', line 84 def initialize(explicit: [], **kwargs) super(**kwargs) @explicit = explicit.to_a.dup.freeze 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 self.fail_fast = false if fail_fast.nil? self.daemon = false if daemon.nil? end |
Instance Attribute Details
#baseline ⇒ Object
Returns the value of attribute baseline
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def baseline @baseline end |
#baseline_epsilon ⇒ Object
Returns the value of attribute baseline_epsilon
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def baseline_epsilon @baseline_epsilon end |
#boot ⇒ Object
Returns the value of attribute boot
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def boot @boot end |
#cache_dir ⇒ Object
Returns the value of attribute cache_dir
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def cache_dir @cache_dir end |
#daemon ⇒ Object
Returns the value of attribute daemon
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def daemon @daemon end |
#daemon_timeout ⇒ Object
Returns the value of attribute daemon_timeout
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def daemon_timeout @daemon_timeout end |
#dry_run ⇒ Object
Returns the value of attribute dry_run
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def dry_run @dry_run end |
#fail_fast ⇒ Object
Returns the value of attribute fail_fast
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def fail_fast @fail_fast end |
#format ⇒ Object
Returns the value of attribute format
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def format @format end |
#framework ⇒ Object
Returns the value of attribute framework
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def framework @framework end |
#ignore ⇒ Object
Returns the value of attribute ignore
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def ignore @ignore end |
#jobs ⇒ Object
Returns the value of attribute jobs
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def jobs @jobs end |
#load_paths ⇒ Object
Returns the value of attribute load_paths
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def load_paths @load_paths end |
#only ⇒ Object
Returns the value of attribute only
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def only @only end |
#operators ⇒ Object
Returns the value of attribute operators
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def operators @operators end |
#output ⇒ Object
Returns the value of attribute output
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def output @output end |
#project_root ⇒ Object
Returns the value of attribute project_root
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def project_root @project_root end |
#rails ⇒ Object
Returns the value of attribute rails
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def rails @rails end |
#require_paths ⇒ Object
Returns the value of attribute require_paths
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def require_paths @require_paths end |
#since ⇒ Object
Returns the value of attribute since
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def since @since end |
#sources ⇒ Object
Returns the value of attribute sources
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def sources @sources end |
#strategy ⇒ Object
Returns the value of attribute strategy
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def strategy @strategy end |
#test_command ⇒ Object
Returns the value of attribute test_command
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def test_command @test_command end |
#tests ⇒ Object
Returns the value of attribute tests
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def tests @tests end |
#threshold ⇒ Object
Returns the value of attribute threshold
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def threshold @threshold end |
#verbose ⇒ Object
Returns the value of attribute verbose
65 66 67 |
# File 'lib/mutineer/config.rb', line 65 def verbose @verbose end |
Class Method Details
.detect_framework(tests) ⇒ String
Pick rspec when a MAJORITY of the given test files end with _spec.rb; otherwise minitest. Empty/ambiguous -> minitest (the safe default).
207 208 209 210 211 |
# File 'lib/mutineer/config.rb', line 207 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) ⇒ Symbol
Maps a config key to its Struct field.
217 218 219 |
# File 'lib/mutineer/config.rb', line 217 def self.field_for(known_key) known_key == "require" ? :require_paths : known_key.to_sym end |
.filter_operators(names, file_name) ⇒ Array<String>
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Drop (with a warning) operator names the registry does not know. 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.
310 311 312 313 314 315 316 317 318 319 |
# File 'lib/mutineer/config.rb', line 310 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. Pure discovery;
reads no file content.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/mutineer/config.rb', line 121 def self.find_file(start = Dir.pwd, home = File.("~")) dir = File.(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 |
.finite_float(value) ⇒ Float?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Reads a finite Float from a number or a plain decimal string. Rejects booleans,
NaN and Infinity: Float(true) is an error, but a YAML .nan is a Float.
A string must be digits with an optional fraction, the same digits-only rule
as jobs: Float() alone would also read 0x10, 1_0, +2 and 1e2.
294 295 296 297 298 299 300 |
# File 'lib/mutineer/config.rb', line 294 def self.finite_float(value) f = case value when Integer, Float then value.to_f when String then Float(value) if value.match?(/\A\d+(\.\d+)?\z/) end f if f&.finite? 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. A YAML syntax error raises ConfigError: never a silent fallback to defaults, and never an exit from the lib layer.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/mutineer/config.rb', line 140 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 field = field_for(ks) parsed = parse(field, value, file: name) parsed = filter_operators(parsed, name) if field == :operators out[field] = parsed end out rescue Psych::SyntaxError => e raise ConfigError, "#{File.basename(path)} parse error: #{e.}" end |
.parse(field, value, file: nil) ⇒ Object
Parses one raw value into the typed value for field. A value that does
not fit its type raises ConfigError naming where it came from, so the CLI
and .mutineer.yml report the same mistake in the same way. nil and
false are valid results for some fields (since: false means "no scoping").
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/mutineer/config.rb', line 232 def self.parse(field, value, file: nil) opt = CONFIG_OPTIONS.find { |o| o.field == field } or raise ArgumentError, "unknown option #{field.inspect}" origin = file ? "#{file}: #{opt.yaml_key}" : opt.flag got = "(got: #{value.inspect})" case opt.type when :positive_int n = value.is_a?(Integer) ? value : (value.to_i if value.is_a?(String) && value.match?(/\A\d+\z/)) raise ConfigError, "#{origin} must be a positive integer, digits only #{got}" if n.nil? || n < 1 n when :percent f = finite_float(value) raise ConfigError, "#{origin} must be a number between 0 and 100 #{got}" unless f && (0.0..100.0).cover?(f) f when :nonneg_float f = finite_float(value) raise ConfigError, "#{origin} must be a finite number, 0 or greater #{got}" unless f && f >= 0.0 f when :bool return value if [true, false].include?(value) return value == "true" if %w[true false].include?(value) raise ConfigError, "#{origin} must be true or false #{got}" when :enum name = opt.aliases&.fetch(value, nil) || value return name if opt.values.include?(name) prefix = file ? "#{file}: " : "" raise ConfigError, "#{prefix}unknown #{field} #{value.to_s.inspect}. Expected: #{opt.values.join(', ')}" when :string_list then Array(value).map(&:to_s) when :string # A key written with no value (`baseline:`) parses as nil. Keeping nil # would switch the feature off without a word; main failed here, so the # typo stays loud. An absent key never reaches parse, so it stays unset. # `only: false` must not become the subject name "false" either: it # matches nothing, so the run has no mutants and still exits 0. raise ConfigError, "#{origin} must be a string #{got}" if [nil, true, false].include?(value) value.to_s when :since # `false` is the one way to say "no scoping" in the file. It becomes nil # so every consumer's nil-check (runner scoping, the report's scoped # marker) agrees; a false left raw would skip scoping but still mark # the report scoped. A blank value is an error, not "no scoping": a # `since: "$REF"` whose variable is unset must not turn scoping off. return nil if value == false raise ConfigError, "#{origin} must be a git ref, not blank #{got}" if value.to_s.strip.empty? value.to_s end end |
.resolve(cli_opts, file_hash) ⇒ Mutineer::Config
Merges the two user layers, CLI over file, then derives what neither wrote.
A layer is a Hash of only the keys the user set, so precedence is merge
and "did the user write this" is whether the key exists. Nothing tracks
provenance by hand, and false/nil are ordinary values.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/mutineer/config.rb', line 174 def self.resolve(cli_opts, file_hash) user = file_hash.merge(cli_opts) config = new(**user, explicit: user.keys) # --rails sugar: boot config/environment. Prefer redefine only for the # in-process path (daemon is whole-file reload only). In-process --rails # shares one test database, so force serial unless --daemon. if config.rails config.boot ||= "config/environment" unless config.daemon || config.explicit?(:strategy) config.strategy = "redefine" end unless config.daemon if config.jobs.to_i > 1 warn "[mutineer] --rails without --daemon runs serially (shared test DB); " \ "forcing --jobs 1. Use --daemon for safe --jobs N." end config.jobs = 1 end end # Auto-detect the framework only when the user wrote none: a value from # either layer 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 |
Instance Method Details
#explicit?(key) ⇒ Boolean
True when the user wrote key, on the command line or in the config
file, whatever the value (false and nil count). The answer comes from
which keys the layers held, so a new option needs no bookkeeping to be
covered.
113 114 115 |
# File 'lib/mutineer/config.rb', line 113 def explicit?(key) @explicit.include?(key) end |