evilution 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +23 -0
  3. data/.rubocop_todo.yml +6 -0
  4. data/CHANGELOG.md +54 -0
  5. data/README.md +76 -3
  6. data/lib/evilution/baseline.rb +5 -4
  7. data/lib/evilution/cache.rb +2 -0
  8. data/lib/evilution/child_output.rb +24 -0
  9. data/lib/evilution/cli/commands/run.rb +9 -0
  10. data/lib/evilution/cli/commands/version.rb +2 -0
  11. data/lib/evilution/cli/parser/options_builder.rb +23 -2
  12. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  13. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  14. data/lib/evilution/compare/diff_extractor.rb +6 -0
  15. data/lib/evilution/compare/fingerprint.rb +15 -72
  16. data/lib/evilution/compare/line_normalizer.rb +72 -0
  17. data/lib/evilution/compare/normalizer.rb +17 -4
  18. data/lib/evilution/config/builders/spec_resolver.rb +15 -0
  19. data/lib/evilution/config/builders/spec_selector.rb +16 -0
  20. data/lib/evilution/config/builders.rb +4 -0
  21. data/lib/evilution/config/env_loader.rb +12 -0
  22. data/lib/evilution/config/file_loader.rb +22 -0
  23. data/lib/evilution/config/sources.rb +14 -0
  24. data/lib/evilution/config/validators/base.rb +37 -0
  25. data/lib/evilution/config/validators/example_targeting_cache.rb +37 -0
  26. data/lib/evilution/config/validators/example_targeting_fallback.rb +22 -0
  27. data/lib/evilution/config/validators/fail_fast.rb +11 -0
  28. data/lib/evilution/config/validators/hooks.rb +12 -0
  29. data/lib/evilution/config/validators/ignore_patterns.rb +16 -0
  30. data/lib/evilution/config/validators/integration.rb +11 -0
  31. data/lib/evilution/config/validators/isolation.rb +19 -0
  32. data/lib/evilution/config/validators/jobs.rb +9 -0
  33. data/lib/evilution/config/validators/preload.rb +13 -0
  34. data/lib/evilution/config/validators/profile.rb +11 -0
  35. data/lib/evilution/config/validators/spec_mappings.rb +56 -0
  36. data/lib/evilution/config/validators/spec_pattern.rb +12 -0
  37. data/lib/evilution/config/validators.rb +4 -0
  38. data/lib/evilution/config.rb +93 -266
  39. data/lib/evilution/feedback/detector.rb +15 -0
  40. data/lib/evilution/feedback/messages.rb +42 -0
  41. data/lib/evilution/feedback.rb +5 -0
  42. data/lib/evilution/integration/crash_detector.rb +2 -2
  43. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  44. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  45. data/lib/evilution/integration/rspec/baseline_runner.rb +16 -0
  46. data/lib/evilution/integration/rspec/crash_detector_lifecycle.rb +17 -0
  47. data/lib/evilution/integration/rspec/example_filter_applier.rb +21 -0
  48. data/lib/evilution/integration/rspec/framework_loader.rb +28 -0
  49. data/lib/evilution/integration/rspec/result_builder.rb +40 -0
  50. data/lib/evilution/integration/rspec/state_guard/example_groups_constants.rb +28 -0
  51. data/lib/evilution/integration/rspec/state_guard/internals.rb +19 -0
  52. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +43 -0
  53. data/lib/evilution/integration/rspec/state_guard/reporter_arrays.rb +32 -0
  54. data/lib/evilution/integration/rspec/state_guard/world_example_groups.rb +20 -0
  55. data/lib/evilution/integration/rspec/state_guard/world_filtered_examples.rb +20 -0
  56. data/lib/evilution/integration/rspec/state_guard/world_sources_by_path.rb +20 -0
  57. data/lib/evilution/integration/rspec/state_guard.rb +40 -0
  58. data/lib/evilution/integration/rspec/test_file_resolver.rb +30 -0
  59. data/lib/evilution/integration/rspec/unresolved_spec_warner.rb +18 -0
  60. data/lib/evilution/integration/rspec.rb +61 -232
  61. data/lib/evilution/isolation/fork.rb +23 -13
  62. data/lib/evilution/isolation/in_process.rb +10 -6
  63. data/lib/evilution/mcp/info_tool/actions/base.rb +22 -0
  64. data/lib/evilution/mcp/info_tool/actions/environment.rb +42 -0
  65. data/lib/evilution/mcp/info_tool/actions/feedback.rb +16 -0
  66. data/lib/evilution/mcp/info_tool/actions/statuses.rb +10 -0
  67. data/lib/evilution/mcp/info_tool/actions/subjects.rb +47 -0
  68. data/lib/evilution/mcp/info_tool/actions/tests.rb +60 -0
  69. data/lib/evilution/mcp/info_tool/actions.rb +16 -0
  70. data/lib/evilution/mcp/info_tool/config_factory.rb +24 -0
  71. data/lib/evilution/mcp/info_tool/error_mapper.rb +15 -0
  72. data/lib/evilution/mcp/info_tool/request_parser.rb +34 -0
  73. data/lib/evilution/mcp/info_tool/response_formatter.rb +24 -0
  74. data/lib/evilution/mcp/info_tool/status_glossary.rb +75 -0
  75. data/lib/evilution/mcp/info_tool.rb +43 -263
  76. data/lib/evilution/mcp/mutate_tool/error_payload.rb +8 -1
  77. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  78. data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +13 -1
  79. data/lib/evilution/mcp/mutate_tool.rb +5 -2
  80. data/lib/evilution/mcp/session_tool.rb +0 -2
  81. data/lib/evilution/mutation.rb +47 -27
  82. data/lib/evilution/mutator/base.rb +8 -8
  83. data/lib/evilution/mutator/operator/block_removal.rb +1 -1
  84. data/lib/evilution/mutator/operator/method_body_replacement.rb +18 -2
  85. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  86. data/lib/evilution/mutator/registry.rb +20 -0
  87. data/lib/evilution/parallel/work_queue/channel/frame.rb +25 -0
  88. data/lib/evilution/parallel/work_queue/channel.rb +23 -0
  89. data/lib/evilution/parallel/work_queue/collection_state.rb +14 -0
  90. data/lib/evilution/parallel/work_queue/dispatcher.rb +133 -0
  91. data/lib/evilution/parallel/work_queue/validators/optional_positive_int.rb +11 -0
  92. data/lib/evilution/parallel/work_queue/validators/optional_positive_number.rb +11 -0
  93. data/lib/evilution/parallel/work_queue/validators/positive_int.rb +11 -0
  94. data/lib/evilution/parallel/work_queue/validators.rb +6 -0
  95. data/lib/evilution/parallel/work_queue/worker/loop.rb +45 -0
  96. data/lib/evilution/parallel/work_queue/worker.rb +114 -0
  97. data/lib/evilution/parallel/work_queue/worker_stat.rb +17 -0
  98. data/lib/evilution/parallel/work_queue.rb +42 -327
  99. data/lib/evilution/process_cleanup.rb +19 -0
  100. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +18 -0
  101. data/lib/evilution/reporter/cli/item_formatters/disabled.rb +9 -0
  102. data/lib/evilution/reporter/cli/item_formatters/error.rb +14 -0
  103. data/lib/evilution/reporter/cli/item_formatters/result_location.rb +10 -0
  104. data/lib/evilution/reporter/cli/item_formatters.rb +6 -0
  105. data/lib/evilution/reporter/cli/line_formatters/duration.rb +9 -0
  106. data/lib/evilution/reporter/cli/line_formatters/efficiency.rb +18 -0
  107. data/lib/evilution/reporter/cli/line_formatters/feedback_footer.rb +13 -0
  108. data/lib/evilution/reporter/cli/line_formatters/header.rb +10 -0
  109. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +16 -0
  110. data/lib/evilution/reporter/cli/line_formatters/peak_memory.rb +12 -0
  111. data/lib/evilution/reporter/cli/line_formatters/result_line.rb +20 -0
  112. data/lib/evilution/reporter/cli/line_formatters/score.rb +14 -0
  113. data/lib/evilution/reporter/cli/line_formatters/truncation_notice.rb +11 -0
  114. data/lib/evilution/reporter/cli/line_formatters.rb +6 -0
  115. data/lib/evilution/reporter/cli/metrics_block.rb +26 -0
  116. data/lib/evilution/reporter/cli/pct.rb +9 -0
  117. data/lib/evilution/reporter/cli/section.rb +13 -0
  118. data/lib/evilution/reporter/cli/section_renderer.rb +15 -0
  119. data/lib/evilution/reporter/cli/trailer.rb +22 -0
  120. data/lib/evilution/reporter/cli.rb +79 -162
  121. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  122. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  123. data/lib/evilution/reporter/html/escape.rb +1 -1
  124. data/lib/evilution/reporter/html/section.rb +1 -1
  125. data/lib/evilution/reporter/html/sections.rb +4 -2
  126. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  127. data/lib/evilution/reporter/html.rb +8 -3
  128. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  129. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  130. data/lib/evilution/reporter/suggestion/templates/minitest.rb +349 -643
  131. data/lib/evilution/reporter/suggestion/templates/rspec.rb +351 -598
  132. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  133. data/lib/evilution/result/error_info.rb +20 -0
  134. data/lib/evilution/result/memory_stats.rb +20 -0
  135. data/lib/evilution/result/mutation_result.rb +30 -14
  136. data/lib/evilution/runner/baseline_runner.rb +1 -2
  137. data/lib/evilution/runner/diagnostics.rb +1 -2
  138. data/lib/evilution/runner/isolation_resolver.rb +10 -4
  139. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +30 -0
  140. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +15 -0
  141. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +39 -0
  142. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +68 -0
  143. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  144. data/lib/evilution/runner/mutation_executor/result_cache.rb +67 -0
  145. data/lib/evilution/runner/mutation_executor/result_notifier.rb +46 -0
  146. data/lib/evilution/runner/mutation_executor/result_packer.rb +41 -0
  147. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +78 -0
  148. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +32 -0
  149. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  150. data/lib/evilution/runner/mutation_executor.rb +53 -292
  151. data/lib/evilution/runner/mutation_planner.rb +1 -2
  152. data/lib/evilution/runner/report_publisher.rb +1 -2
  153. data/lib/evilution/runner/subject_pipeline.rb +1 -2
  154. data/lib/evilution/runner.rb +53 -30
  155. data/lib/evilution/version.rb +1 -1
  156. data/lib/evilution.rb +1 -0
  157. data/script/memory_check +3 -1
  158. metadata +125 -3
  159. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../compare"
4
+
5
+ # Collapses whitespace runs in source-code text while preserving the contents
6
+ # of "..." and '...' string literals. Used for fingerprinting mutation diffs
7
+ # so that whitespace-only differences do not cause false fingerprint mismatches
8
+ # across tooling (evilution vs mutant).
9
+ #
10
+ # v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
11
+ # heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
12
+ # inside them collapse. A mutation touching whitespace inside a regex may
13
+ # false-match across tools.
14
+ class Evilution::Compare::LineNormalizer
15
+ QUOTES = ['"', "'"].freeze
16
+ WHITESPACE = [" ", "\t"].freeze
17
+ private_constant :QUOTES, :WHITESPACE
18
+
19
+ def call(line)
20
+ @chars = line.chars
21
+ @i = 0
22
+ @out = +""
23
+ @in_literal = nil
24
+ @last_was_space = false
25
+
26
+ @i += step while @i < @chars.length
27
+ result = @out.rstrip
28
+ @chars = nil
29
+ @out = nil
30
+ result
31
+ end
32
+
33
+ private
34
+
35
+ def step
36
+ ch = @chars[@i]
37
+ return step_in_literal(ch) if @in_literal
38
+ return step_open_quote(ch) if QUOTES.include?(ch)
39
+ return step_whitespace if WHITESPACE.include?(ch)
40
+
41
+ append_regular(ch)
42
+ end
43
+
44
+ def step_in_literal(ch)
45
+ @out << ch
46
+ if ch == "\\" && @i + 1 < @chars.length
47
+ @out << @chars[@i + 1]
48
+ return 2
49
+ end
50
+ @in_literal = nil if ch == @in_literal
51
+ 1
52
+ end
53
+
54
+ def step_open_quote(ch)
55
+ @in_literal = ch
56
+ @out << ch
57
+ @last_was_space = false
58
+ 1
59
+ end
60
+
61
+ def step_whitespace
62
+ @out << " " unless @last_was_space || @out.empty?
63
+ @last_was_space = true
64
+ 1
65
+ end
66
+
67
+ def append_regular(ch)
68
+ @out << ch
69
+ @last_was_space = false
70
+ 1
71
+ end
72
+ end
@@ -3,6 +3,9 @@
3
3
  require_relative "../compare"
4
4
  require_relative "record"
5
5
  require_relative "fingerprint"
6
+ require_relative "line_normalizer"
7
+ require_relative "diff_extractor/evilution"
8
+ require_relative "diff_extractor/mutant"
6
9
 
7
10
  class Evilution::Compare::Normalizer
8
11
  EVILUTION_BUCKETS = %w[killed survived timed_out errors neutral equivalent unresolved unparseable].freeze
@@ -17,6 +20,18 @@ class Evilution::Compare::Normalizer
17
20
  "unparseable" => :unparseable
18
21
  }.freeze
19
22
 
23
+ def initialize
24
+ line_normalizer = Evilution::Compare::LineNormalizer.new
25
+ @evilution_fingerprint = Evilution::Compare::Fingerprint.new(
26
+ extractor: Evilution::Compare::DiffExtractor::Evilution.new,
27
+ normalizer: line_normalizer
28
+ )
29
+ @mutant_fingerprint = Evilution::Compare::Fingerprint.new(
30
+ extractor: Evilution::Compare::DiffExtractor::Mutant.new,
31
+ normalizer: line_normalizer
32
+ )
33
+ end
34
+
20
35
  def from_evilution(json)
21
36
  records = []
22
37
  EVILUTION_BUCKETS.each do |bucket|
@@ -47,13 +62,12 @@ class Evilution::Compare::Normalizer
47
62
  diff = entry["diff"].to_s
48
63
  status = EVILUTION_STATUS_MAP[entry["status"]] ||
49
64
  raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
50
- body = Evilution::Compare::Fingerprint.extract_from_evilution_diff(diff)
51
65
  Evilution::Compare::Record.new(
52
66
  source: :evilution,
53
67
  file_path: file_path,
54
68
  line: line,
55
69
  status: status,
56
- fingerprint: Evilution::Compare::Fingerprint.compute(file_path: file_path, line: line, body: body),
70
+ fingerprint: @evilution_fingerprint.call(diff: diff, file_path: file_path, line: line),
57
71
  operator: entry["operator"],
58
72
  diff_body: diff,
59
73
  raw: entry
@@ -67,13 +81,12 @@ class Evilution::Compare::Normalizer
67
81
  line = parse_mutant_line(ident, index)
68
82
  diff = mr["mutation_diff"].to_s
69
83
  status = derive_mutant_status(mr, cr, index)
70
- body = Evilution::Compare::Fingerprint.extract_from_mutant_diff(diff)
71
84
  Evilution::Compare::Record.new(
72
85
  source: :mutant,
73
86
  file_path: source_path,
74
87
  line: line,
75
88
  status: status,
76
- fingerprint: Evilution::Compare::Fingerprint.compute(file_path: source_path, line: line, body: body),
89
+ fingerprint: @mutant_fingerprint.call(diff: diff, file_path: source_path, line: line),
77
90
  operator: nil,
78
91
  diff_body: diff,
79
92
  raw: { "mutation_result" => mr, "criteria_result" => cr, "source_path" => source_path }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../builders"
4
+ require_relative "../../spec_resolver"
5
+
6
+ class Evilution::Config::Builders::SpecResolver
7
+ def self.call(integration:)
8
+ case integration
9
+ when :minitest
10
+ Evilution::SpecResolver.new(test_dir: "test", test_suffix: "_test.rb", request_dir: "integration")
11
+ else
12
+ Evilution::SpecResolver.new
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../builders"
4
+ require_relative "spec_resolver"
5
+ require_relative "../../spec_selector"
6
+
7
+ class Evilution::Config::Builders::SpecSelector
8
+ def self.call(spec_files:, spec_mappings:, spec_pattern:, integration:)
9
+ Evilution::SpecSelector.new(
10
+ spec_files: spec_files,
11
+ spec_mappings: spec_mappings,
12
+ spec_pattern: spec_pattern,
13
+ spec_resolver: Evilution::Config::Builders::SpecResolver.call(integration: integration)
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Config::Builders
4
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Config::EnvLoader
4
+ module_function
5
+
6
+ def load
7
+ opts = {}
8
+ val = ENV.fetch("EV_DISABLE_EXAMPLE_TARGETING", nil)
9
+ opts[:example_targeting] = false if val && !val.empty? && val != "0"
10
+ opts
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Evilution::Config::FileLoader
6
+ module_function
7
+
8
+ def load
9
+ Evilution::Config::CONFIG_FILES.each do |path|
10
+ next unless File.exist?(path)
11
+
12
+ data = YAML.safe_load_file(path, symbolize_names: true)
13
+ return data.is_a?(Hash) ? data : {}
14
+ rescue Psych::SyntaxError, Psych::DisallowedClass => e
15
+ raise Evilution::ConfigError.new("failed to parse config file #{path}: #{e.message}", file: path)
16
+ rescue SystemCallError => e
17
+ raise Evilution::ConfigError.new("cannot read config file #{path}: #{e.message}", file: path)
18
+ end
19
+
20
+ {}
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "file_loader"
4
+ require_relative "env_loader"
5
+
6
+ module Evilution::Config::Sources
7
+ module_function
8
+
9
+ def merge(explicit:, skip_file:)
10
+ file = skip_file ? {} : Evilution::Config::FileLoader.load
11
+ env = Evilution::Config::EnvLoader.load
12
+ Evilution::Config::DEFAULTS.merge(file).merge(env).merge(explicit)
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validators"
4
+
5
+ class Evilution::Config::Validators::Base
6
+ def self.call(_value)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ class << self
11
+ private
12
+
13
+ def coerce_symbol!(value, allowed:, name:)
14
+ raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got nil" if value.nil?
15
+
16
+ unless value.is_a?(String) || value.is_a?(Symbol)
17
+ raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{value.inspect}"
18
+ end
19
+
20
+ sym = value.to_sym
21
+ return sym if allowed.include?(sym)
22
+
23
+ raise Evilution::ConfigError, "#{name} must be #{allowed.join(" or ")}, got #{sym.inspect}"
24
+ end
25
+
26
+ def coerce_positive_int!(value, name:)
27
+ raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}" if value.is_a?(Float)
28
+
29
+ int = Integer(value)
30
+ raise Evilution::ConfigError, "#{name} must be a positive integer, got #{int}" unless int >= 1
31
+
32
+ int
33
+ rescue ::ArgumentError, ::TypeError
34
+ raise Evilution::ConfigError, "#{name} must be a positive integer, got #{value.inspect}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::ExampleTargetingCache < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ raise Evilution::ConfigError, "example_targeting_cache must be a Hash, got #{value.class}" unless value.is_a?(Hash)
8
+
9
+ normalized = normalize_keys(value)
10
+ merged = Evilution::Config::DEFAULTS[:example_targeting_cache].merge(normalized)
11
+ require_positive_int!(merged, :max_files)
12
+ require_positive_int!(merged, :max_blocks)
13
+ merged
14
+ end
15
+
16
+ class << self
17
+ private
18
+
19
+ def normalize_keys(value)
20
+ value.each_with_object({}) do |(k, v), acc|
21
+ unless k.is_a?(String) || k.is_a?(Symbol)
22
+ raise Evilution::ConfigError,
23
+ "example_targeting_cache keys must be Strings or Symbols, got #{k.inspect}"
24
+ end
25
+ acc[k.to_sym] = v
26
+ end
27
+ end
28
+
29
+ def require_positive_int!(cache, key)
30
+ v = cache[key]
31
+ return if v.is_a?(Integer) && v >= 1
32
+
33
+ raise Evilution::ConfigError,
34
+ "example_targeting_cache.#{key} must be a positive integer, got #{v.inspect}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::ExampleTargetingFallback < Evilution::Config::Validators::Base
6
+ FALLBACKS = %i[full_file unresolved].freeze
7
+
8
+ def self.call(value)
9
+ unless value.is_a?(String) || value.is_a?(Symbol)
10
+ raise Evilution::ConfigError,
11
+ "example_targeting_fallback must be full_file or unresolved, got #{value.inspect}"
12
+ end
13
+
14
+ sym = value.to_sym
15
+ unless FALLBACKS.include?(sym)
16
+ raise Evilution::ConfigError,
17
+ "example_targeting_fallback must be full_file or unresolved, got #{sym.inspect}"
18
+ end
19
+
20
+ sym
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::FailFast < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+
9
+ coerce_positive_int!(value, name: "fail_fast")
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Hooks < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return {} if value.nil?
8
+ raise Evilution::ConfigError, "hooks must be a mapping of event names to file paths, got #{value.class}" unless value.is_a?(Hash)
9
+
10
+ value
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::IgnorePatterns < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ patterns = Array(value)
8
+ patterns.each do |pattern|
9
+ unless pattern.is_a?(String)
10
+ raise Evilution::ConfigError,
11
+ "ignore_patterns must be an array of strings, got #{pattern.class} (#{pattern.inspect})"
12
+ end
13
+ end
14
+ patterns
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Integration < Evilution::Config::Validators::Base
6
+ ALLOWED = %i[rspec minitest].freeze
7
+
8
+ def self.call(value)
9
+ coerce_symbol!(value, allowed: ALLOWED, name: "integration")
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Isolation < Evilution::Config::Validators::Base
6
+ ALLOWED = %i[auto fork in_process].freeze
7
+ MESSAGE = "isolation must be auto, fork, or in_process"
8
+
9
+ def self.call(value)
10
+ raise Evilution::ConfigError, "#{MESSAGE}, got nil" if value.nil?
11
+
12
+ raise Evilution::ConfigError, "#{MESSAGE}, got #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
13
+
14
+ sym = value.to_sym
15
+ return sym if ALLOWED.include?(sym)
16
+
17
+ raise Evilution::ConfigError, "#{MESSAGE}, got #{sym.inspect}"
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Jobs < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ coerce_positive_int!(value, name: "jobs")
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Preload < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+ return false if value == false
9
+ return value if value.is_a?(String)
10
+
11
+ raise Evilution::ConfigError, "preload must be nil, false, or a String path, got #{value.inspect}"
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::Profile < Evilution::Config::Validators::Base
6
+ ALLOWED = %i[default strict].freeze
7
+
8
+ def self.call(value)
9
+ coerce_symbol!(value, allowed: ALLOWED, name: "profile")
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::SpecMappings < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return {} if value.nil?
8
+
9
+ raise Evilution::ConfigError, "spec_mappings must be a Hash, got #{value.class}" unless value.is_a?(Hash)
10
+
11
+ normalized = value.each_with_object({}) do |(source, specs), acc|
12
+ key = normalize_key(source)
13
+ acc[key] = normalize_value(key, specs)
14
+ end
15
+
16
+ warn_missing(normalized)
17
+ normalized
18
+ end
19
+
20
+ class << self
21
+ private
22
+
23
+ def normalize_key(source)
24
+ key = source.to_s
25
+ key = key.delete_prefix("#{Dir.pwd}/") if key.start_with?("/")
26
+ key.delete_prefix("./")
27
+ end
28
+
29
+ def normalize_value(source, specs)
30
+ case specs
31
+ when String then [specs]
32
+ when Array
33
+ specs.each do |entry|
34
+ unless entry.is_a?(String)
35
+ raise Evilution::ConfigError,
36
+ "spec_mappings[#{source.inspect}] entries must be string paths, got #{entry.class}"
37
+ end
38
+ end
39
+ specs
40
+ else
41
+ raise Evilution::ConfigError,
42
+ "spec_mappings[#{source.inspect}] must be a string or array of strings, got #{specs.class}"
43
+ end
44
+ end
45
+
46
+ def warn_missing(mappings)
47
+ mappings.each do |source, specs|
48
+ specs.each do |spec_path|
49
+ next if File.exist?(spec_path)
50
+
51
+ warn "[evilution] spec_mappings[#{source.inspect}]: #{spec_path} not found, skipping"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Evilution::Config::Validators::SpecPattern < Evilution::Config::Validators::Base
6
+ def self.call(value)
7
+ return nil if value.nil?
8
+ return value if value.is_a?(String)
9
+
10
+ raise Evilution::ConfigError, "spec_pattern must be nil or a String glob, got #{value.class}"
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution::Config::Validators
4
+ end