evilution 0.27.0 → 0.29.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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/interactions.jsonl +65 -0
  3. data/.rubocop_todo.yml +0 -1
  4. data/CHANGELOG.md +39 -0
  5. data/README.md +19 -0
  6. data/lib/evilution/ast/constant_names.rb +28 -11
  7. data/lib/evilution/ast/pattern/parser.rb +29 -17
  8. data/lib/evilution/baseline.rb +5 -4
  9. data/lib/evilution/cli/commands/session_diff.rb +6 -4
  10. data/lib/evilution/cli/commands/subjects.rb +6 -3
  11. data/lib/evilution/cli/commands/util_mutation.rb +24 -19
  12. data/lib/evilution/cli/parser/command_extractor.rb +9 -11
  13. data/lib/evilution/cli/parser/file_args.rb +3 -1
  14. data/lib/evilution/cli/parser/options_builder.rb +36 -1
  15. data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
  16. data/lib/evilution/cli/parser.rb +18 -20
  17. data/lib/evilution/cli/printers/environment.rb +19 -19
  18. data/lib/evilution/cli/printers/session_diff.rb +8 -8
  19. data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
  20. data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
  21. data/lib/evilution/compare/diff_extractor.rb +6 -0
  22. data/lib/evilution/compare/fingerprint.rb +15 -72
  23. data/lib/evilution/compare/line_normalizer.rb +72 -0
  24. data/lib/evilution/compare/normalizer.rb +27 -9
  25. data/lib/evilution/config/validators/profile.rb +11 -0
  26. data/lib/evilution/config.rb +49 -32
  27. data/lib/evilution/disable_comment.rb +21 -12
  28. data/lib/evilution/integration/crash_detector.rb +2 -2
  29. data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
  30. data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
  31. data/lib/evilution/integration/minitest.rb +25 -16
  32. data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
  33. data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
  34. data/lib/evilution/integration/rspec.rb +4 -0
  35. data/lib/evilution/isolation/fork.rb +43 -28
  36. data/lib/evilution/isolation/in_process.rb +10 -6
  37. data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
  38. data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
  39. data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
  40. data/lib/evilution/mcp/info_tool.rb +7 -3
  41. data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
  42. data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
  43. data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
  44. data/lib/evilution/mcp/mutate_tool.rb +27 -14
  45. data/lib/evilution/mcp/session_tool.rb +27 -20
  46. data/lib/evilution/mutation.rb +60 -42
  47. data/lib/evilution/mutator/base.rb +23 -21
  48. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
  49. data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
  50. data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
  51. data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
  52. data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
  53. data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
  54. data/lib/evilution/mutator/operator/case_when.rb +7 -5
  55. data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
  56. data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
  57. data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
  58. data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
  59. data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
  60. data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
  61. data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
  62. data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
  63. data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
  64. data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
  65. data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
  66. data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
  67. data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
  68. data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
  69. data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
  70. data/lib/evilution/mutator/registry.rb +20 -0
  71. data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
  72. data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
  73. data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
  74. data/lib/evilution/parallel/work_queue/worker.rb +10 -7
  75. data/lib/evilution/parallel/work_queue.rb +35 -18
  76. data/lib/evilution/process_cleanup.rb +19 -0
  77. data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
  78. data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
  79. data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
  80. data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
  81. data/lib/evilution/reporter/html/escape.rb +1 -1
  82. data/lib/evilution/reporter/html/section.rb +1 -1
  83. data/lib/evilution/reporter/html/sections.rb +4 -2
  84. data/lib/evilution/reporter/html/stylesheet.rb +1 -1
  85. data/lib/evilution/reporter/html.rb +8 -3
  86. data/lib/evilution/reporter/json.rb +52 -18
  87. data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
  88. data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
  89. data/lib/evilution/reporter/suggestion/registry.rb +1 -5
  90. data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
  91. data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
  92. data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
  93. data/lib/evilution/reporter/suggestion/templates.rb +6 -0
  94. data/lib/evilution/result/error_info.rb +20 -0
  95. data/lib/evilution/result/memory_stats.rb +20 -0
  96. data/lib/evilution/result/mutation_result.rb +30 -14
  97. data/lib/evilution/runner/baseline_runner.rb +16 -10
  98. data/lib/evilution/runner/diagnostics.rb +14 -11
  99. data/lib/evilution/runner/isolation_resolver.rb +12 -11
  100. data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
  101. data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
  102. data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
  103. data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
  104. data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
  105. data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
  106. data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
  107. data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
  108. data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
  109. data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
  110. data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
  111. data/lib/evilution/runner/mutation_executor.rb +14 -20
  112. data/lib/evilution/runner/mutation_planner.rb +38 -19
  113. data/lib/evilution/runner/report_publisher.rb +1 -2
  114. data/lib/evilution/runner/subject_pipeline.rb +22 -13
  115. data/lib/evilution/runner.rb +36 -34
  116. data/lib/evilution/session/diff.rb +15 -6
  117. data/lib/evilution/spec_ast_cache.rb +26 -12
  118. data/lib/evilution/version.rb +1 -1
  119. data/lib/evilution.rb +1 -0
  120. data/script/memory_check +14 -6
  121. data/scripts/benchmark_density +10 -9
  122. data/scripts/compare_mutations +38 -21
  123. data/scripts/mutant_json_adapter +7 -4
  124. metadata +15 -3
  125. data/lib/evilution/reporter/html/namespace.rb +0 -11
@@ -20,7 +20,9 @@ class Evilution::CLI::Parser
20
20
 
21
21
  preprocess_flags
22
22
  remaining = OptionsBuilder.build(@options).parse!(@argv)
23
- @files, @line_ranges = FileArgs.parse(remaining)
23
+ parsed_paths = FileArgs.parse(remaining)
24
+ @files = parsed_paths.files
25
+ @line_ranges = parsed_paths.ranges
24
26
  read_stdin_files if @options.delete(:stdin) && %i[run subjects].include?(@command)
25
27
  build_parsed_args
26
28
  end
@@ -37,27 +39,23 @@ class Evilution::CLI::Parser
37
39
  def preprocess_flags
38
40
  result = []
39
41
  i = 0
40
- while i < @argv.length
41
- arg = @argv[i]
42
- if arg == "--fail-fast"
43
- next_arg = @argv[i + 1]
42
+ i = consume_token(i, result) while i < @argv.length
43
+ @argv = result
44
+ end
44
45
 
45
- if next_arg && next_arg.match?(/\A-?\d+\z/)
46
- @options[:fail_fast] = next_arg
47
- i += 2
48
- else
49
- result << arg
50
- i += 1
51
- end
52
- elsif arg.start_with?("--fail-fast=")
53
- @options[:fail_fast] = arg.delete_prefix("--fail-fast=")
54
- i += 1
55
- else
56
- result << arg
57
- i += 1
58
- end
46
+ def consume_token(i, result)
47
+ arg = @argv[i]
48
+ next_arg = @argv[i + 1]
49
+ if arg == "--fail-fast" && !next_arg.nil? && next_arg.match?(/\A-?\d+\z/)
50
+ @options[:fail_fast] = next_arg
51
+ return i + 2
59
52
  end
60
- @argv = result
53
+ if arg.start_with?("--fail-fast=")
54
+ @options[:fail_fast] = arg.delete_prefix("--fail-fast=")
55
+ return i + 1
56
+ end
57
+ result << arg
58
+ i + 1
61
59
  end
62
60
 
63
61
  def read_stdin_files
@@ -3,6 +3,12 @@
3
3
  require_relative "../printers"
4
4
 
5
5
  class Evilution::CLI::Printers::Environment
6
+ PLAIN_SETTINGS = %i[
7
+ timeout format integration jobs isolation baseline incremental
8
+ verbose quiet progress min_score suggest_tests save_session
9
+ skip_heredoc_literals
10
+ ].freeze
11
+
6
12
  def initialize(config, config_file:)
7
13
  @config = config
8
14
  @config_file = config_file
@@ -30,24 +36,18 @@ class Evilution::CLI::Printers::Environment
30
36
  end
31
37
 
32
38
  def settings_lines
33
- [
34
- " timeout: #{@config.timeout}",
35
- " format: #{@config.format}",
36
- " integration: #{@config.integration}",
37
- " jobs: #{@config.jobs}",
38
- " isolation: #{@config.isolation}",
39
- " baseline: #{@config.baseline}",
40
- " incremental: #{@config.incremental}",
41
- " verbose: #{@config.verbose}",
42
- " quiet: #{@config.quiet}",
43
- " progress: #{@config.progress}",
44
- " fail_fast: #{@config.fail_fast || "(disabled)"}",
45
- " min_score: #{@config.min_score}",
46
- " suggest_tests: #{@config.suggest_tests}",
47
- " save_session: #{@config.save_session}",
48
- " target: #{@config.target || "(all files)"}",
49
- " skip_heredoc_literals: #{@config.skip_heredoc_literals}",
50
- " ignore_patterns: #{@config.ignore_patterns.empty? ? "(none)" : @config.ignore_patterns.inspect}"
51
- ]
39
+ plain_lines = PLAIN_SETTINGS.map { |k| setting_line(k, @config.public_send(k)) }
40
+ plain_lines.insert(10, setting_line(:fail_fast, @config.fail_fast || "(disabled)"))
41
+ plain_lines.insert(14, setting_line(:target, @config.target || "(all files)"))
42
+ plain_lines << setting_line(:ignore_patterns, format_ignore_patterns(@config.ignore_patterns))
43
+ plain_lines
44
+ end
45
+
46
+ def setting_line(key, value)
47
+ " #{key}: #{value}"
48
+ end
49
+
50
+ def format_ignore_patterns(patterns)
51
+ patterns.empty? ? "(none)" : patterns.inspect
52
52
  end
53
53
  end
@@ -32,16 +32,16 @@ class Evilution::CLI::Printers::SessionDiff
32
32
  end
33
33
 
34
34
  def print_summary(io, summary)
35
- delta_str = format("%+.2f%%", summary.score_delta * 100)
36
35
  io.puts("Session Diff")
37
36
  io.puts("=" * 40)
38
- io.puts(format("Base score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
39
- score: summary.base_score * 100, killed: summary.base_killed,
40
- total: summary.base_total))
41
- io.puts(format("Head score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
42
- score: summary.head_score * 100, killed: summary.head_killed,
43
- total: summary.head_total))
44
- io.puts("Delta: #{delta_str}")
37
+ io.puts(score_line("Base", summary.base_score, summary.base_killed, summary.base_total))
38
+ io.puts(score_line("Head", summary.head_score, summary.head_killed, summary.head_total))
39
+ io.puts("Delta: #{format("%+.2f%%", summary.score_delta * 100)}")
40
+ end
41
+
42
+ def score_line(label, score, killed, total)
43
+ format("%<label>s score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
44
+ label: label, score: score * 100, killed: killed, total: total)
45
45
  end
46
46
 
47
47
  def print_section(io, title, mutations, color)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diff_extractor"
4
+
5
+ # Extracts {minus:, plus:} payload arrays from Evilution-format diffs.
6
+ # Evilution diffs use "- " / "+ " line prefixes (note the trailing space) and
7
+ # do not carry unified-diff headers or hunk markers.
8
+ class Evilution::Compare::DiffExtractor::Evilution
9
+ def call(diff)
10
+ minus = []
11
+ plus = []
12
+ diff.to_s.each_line do |line|
13
+ line = line.chomp
14
+ if line.start_with?("- ")
15
+ minus << line[2..]
16
+ elsif line.start_with?("+ ")
17
+ plus << line[2..]
18
+ end
19
+ end
20
+ { minus: minus, plus: plus }
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diff_extractor"
4
+
5
+ # Extracts {minus:, plus:} payload arrays from Mutant unified-diff format.
6
+ # Skips the "--- <name>", "+++ <name>", and "@@ ... @@" header lines and
7
+ # returns each remaining payload line with its single leading "-" or "+"
8
+ # marker stripped.
9
+ #
10
+ # Header detection requires a trailing space after "---"/"+++" so that a
11
+ # payload line whose mutated source starts with "--" (emitted as "---var")
12
+ # or "++" (emitted as "+++var") is preserved rather than misclassified as
13
+ # a header.
14
+ class Evilution::Compare::DiffExtractor::Mutant
15
+ def call(diff)
16
+ minus = []
17
+ plus = []
18
+ diff.to_s.each_line do |line|
19
+ line = line.chomp
20
+ next if line.start_with?("--- ", "+++ ", "@@")
21
+
22
+ if line.start_with?("-")
23
+ minus << line[1..]
24
+ elsif line.start_with?("+")
25
+ plus << line[1..]
26
+ end
27
+ end
28
+ { minus: minus, plus: plus }
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../compare"
4
+
5
+ module Evilution::Compare::DiffExtractor
6
+ end
@@ -3,80 +3,23 @@
3
3
  require "digest"
4
4
  require_relative "../compare"
5
5
 
6
- module Evilution::Compare::Fingerprint
7
- module_function
8
-
9
- def extract_from_evilution_diff(diff)
10
- minus = []
11
- plus = []
12
- diff.to_s.each_line do |line|
13
- line = line.chomp
14
- if line.start_with?("- ")
15
- minus << line[2..]
16
- elsif line.start_with?("+ ")
17
- plus << line[2..]
18
- end
19
- end
20
- { minus: minus, plus: plus }
21
- end
22
-
23
- def extract_from_mutant_diff(diff)
24
- minus = []
25
- plus = []
26
- diff.to_s.each_line do |line|
27
- line = line.chomp
28
- next if line.start_with?("---", "+++", "@@")
29
-
30
- if line.start_with?("-")
31
- minus << line[1..]
32
- elsif line.start_with?("+")
33
- plus << line[1..]
34
- end
35
- end
36
- { minus: minus, plus: plus }
37
- end
38
-
39
- # v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
40
- # heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
41
- # inside them collapse. A mutation touching whitespace inside a regex may
42
- # false-match across tools.
43
- # rubocop:disable Metrics/PerceivedComplexity, Style/MultipleComparison
44
- def normalize_line(line)
45
- out = +""
46
- i = 0
47
- in_literal = nil
48
- last_was_space = false
49
- chars = line.chars
50
- while i < chars.length
51
- ch = chars[i]
52
- if in_literal
53
- out << ch
54
- if ch == "\\" && i + 1 < chars.length
55
- out << chars[i + 1]
56
- i += 2
57
- next
58
- end
59
- in_literal = nil if ch == in_literal
60
- elsif ch == '"' || ch == "'"
61
- in_literal = ch
62
- out << ch
63
- last_was_space = false
64
- elsif ch == " " || ch == "\t"
65
- out << " " unless last_was_space || out.empty?
66
- last_was_space = true
67
- else
68
- out << ch
69
- last_was_space = false
70
- end
71
- i += 1
72
- end
73
- out.rstrip
6
+ # Composes a stable SHA256 fingerprint from a mutation diff for cross-tool
7
+ # matching (Evilution vs Mutant). Orchestrates two collaborators along
8
+ # distinct change axes:
9
+ #
10
+ # - extractor: parses a tool-specific diff format into {minus:, plus:}
11
+ # - normalizer: collapses whitespace per line so cosmetic differences
12
+ # don't perturb the hash
13
+ class Evilution::Compare::Fingerprint
14
+ def initialize(extractor:, normalizer:)
15
+ @extractor = extractor
16
+ @normalizer = normalizer
74
17
  end
75
- # rubocop:enable Metrics/PerceivedComplexity, Style/MultipleComparison
76
18
 
77
- def compute(file_path:, line:, body:)
78
- minus = body[:minus].map { |l| normalize_line(l) }
79
- plus = body[:plus].map { |l| normalize_line(l) }
19
+ def call(diff:, file_path:, line:)
20
+ body = @extractor.call(diff)
21
+ minus = body[:minus].map { |l| @normalizer.call(l) }
22
+ plus = body[:plus].map { |l| @normalizer.call(l) }
80
23
  payload = [file_path, line.to_s, minus.join("\n"), plus.join("\n")].join("\x00")
81
24
  Digest::SHA256.hexdigest(payload)
82
25
  end
@@ -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|
@@ -42,24 +57,28 @@ class Evilution::Compare::Normalizer
42
57
  private
43
58
 
44
59
  def build_evilution_record(entry, index:)
45
- file_path = entry["file"] or raise Evilution::Compare::InvalidInput.new("missing 'file' in record", index: index)
46
- line = entry["line"] or raise Evilution::Compare::InvalidInput.new("missing 'line' in record", index: index)
47
- diff = entry["diff"].to_s
48
- status = EVILUTION_STATUS_MAP[entry["status"]] ||
49
- raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
50
- body = Evilution::Compare::Fingerprint.extract_from_evilution_diff(diff)
60
+ file_path, line, diff, status = extract_evilution_fields(entry, index)
51
61
  Evilution::Compare::Record.new(
52
62
  source: :evilution,
53
63
  file_path: file_path,
54
64
  line: line,
55
65
  status: status,
56
- fingerprint: Evilution::Compare::Fingerprint.compute(file_path: file_path, line: line, body: body),
66
+ fingerprint: @evilution_fingerprint.call(diff: diff, file_path: file_path, line: line),
57
67
  operator: entry["operator"],
58
68
  diff_body: diff,
59
69
  raw: entry
60
70
  )
61
71
  end
62
72
 
73
+ def extract_evilution_fields(entry, index)
74
+ file_path = entry["file"] or raise Evilution::Compare::InvalidInput.new("missing 'file' in record", index: index)
75
+ line = entry["line"] or raise Evilution::Compare::InvalidInput.new("missing 'line' in record", index: index)
76
+ diff = entry["diff"].to_s
77
+ status = EVILUTION_STATUS_MAP[entry["status"]] ||
78
+ raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
79
+ [file_path, line, diff, status]
80
+ end
81
+
63
82
  def build_mutant_record(cov, source_path:, index:)
64
83
  mr = cov["mutation_result"] or raise Evilution::Compare::InvalidInput.new("missing mutation_result", index: index)
65
84
  cr = cov["criteria_result"] or raise Evilution::Compare::InvalidInput.new("missing criteria_result", index: index)
@@ -67,13 +86,12 @@ class Evilution::Compare::Normalizer
67
86
  line = parse_mutant_line(ident, index)
68
87
  diff = mr["mutation_diff"].to_s
69
88
  status = derive_mutant_status(mr, cr, index)
70
- body = Evilution::Compare::Fingerprint.extract_from_mutant_diff(diff)
71
89
  Evilution::Compare::Record.new(
72
90
  source: :mutant,
73
91
  file_path: source_path,
74
92
  line: line,
75
93
  status: status,
76
- fingerprint: Evilution::Compare::Fingerprint.compute(file_path: source_path, line: line, body: body),
94
+ fingerprint: @mutant_fingerprint.call(diff: diff, file_path: source_path, line: line),
77
95
  operator: nil,
78
96
  diff_body: diff,
79
97
  raw: { "mutation_result" => mr, "criteria_result" => cr, "source_path" => source_path }
@@ -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
@@ -17,7 +17,8 @@ class Evilution::Config
17
17
  spec_mappings: {}, spec_pattern: nil, example_targeting: true,
18
18
  example_targeting_fallback: :full_file,
19
19
  example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
20
- quiet_children: false, quiet_children_dir: "tmp/evilution_children"
20
+ quiet_children: false, quiet_children_dir: "tmp/evilution_children",
21
+ profile: :default
21
22
  }.freeze
22
23
 
23
24
  attr_reader :target_files, :timeout, :format,
@@ -28,7 +29,7 @@ class Evilution::Config
28
29
  :skip_heredoc_literals, :related_specs_heuristic,
29
30
  :fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
30
31
  :example_targeting, :example_targeting_fallback, :example_targeting_cache,
31
- :spec_selector, :quiet_children, :quiet_children_dir
32
+ :spec_selector, :quiet_children, :quiet_children_dir, :profile
32
33
 
33
34
  def initialize(**options)
34
35
  skip_file = options.delete(:skip_config_file) ? true : false
@@ -183,6 +184,11 @@ class Evilution::Config
183
184
  # ignore_patterns:
184
185
  # - "call{name=info, receiver=call{name=logger}}"
185
186
  # - "call{name=debug|warn}"
187
+
188
+ # Operator profile: default or strict (default: default).
189
+ # strict adds aggressive truthiness mutators (e.g. replaces
190
+ # `x.predicate?` with `nil`) intended for pre-merge audits.
191
+ # profile: default
186
192
  YAML
187
193
  end
188
194
 
@@ -200,40 +206,50 @@ class Evilution::Config
200
206
  )
201
207
  end
202
208
 
209
+ SIMPLE_ATTR_TRANSFORMS = {
210
+ target_files: ->(v) { Array(v) },
211
+ timeout: nil,
212
+ format: :to_sym.to_proc,
213
+ target: nil,
214
+ min_score: :to_f.to_proc,
215
+ verbose: nil,
216
+ quiet: nil,
217
+ baseline: nil,
218
+ incremental: nil,
219
+ suggest_tests: nil,
220
+ progress: nil,
221
+ save_session: nil,
222
+ line_ranges: ->(v) { v || {} },
223
+ spec_files: ->(v) { Array(v) },
224
+ show_disabled: nil,
225
+ baseline_session: nil,
226
+ skip_heredoc_literals: nil,
227
+ related_specs_heuristic: nil,
228
+ fallback_to_full_suite: nil,
229
+ quiet_children: nil,
230
+ quiet_children_dir: nil
231
+ }.freeze
232
+ private_constant :SIMPLE_ATTR_TRANSFORMS
233
+
203
234
  def assign_simple_attributes(merged)
204
- @target_files = Array(merged[:target_files])
205
- @timeout = merged[:timeout]
206
- @format = merged[:format].to_sym
207
- @target = merged[:target]
208
- @min_score = merged[:min_score].to_f
209
- @verbose = merged[:verbose]
210
- @quiet = merged[:quiet]
211
- @baseline = merged[:baseline]
212
- @incremental = merged[:incremental]
213
- @suggest_tests = merged[:suggest_tests]
214
- @progress = merged[:progress]
215
- @save_session = merged[:save_session]
216
- @line_ranges = merged[:line_ranges] || {}
217
- @spec_files = Array(merged[:spec_files])
218
- @show_disabled = merged[:show_disabled]
219
- @baseline_session = merged[:baseline_session]
220
- @skip_heredoc_literals = merged[:skip_heredoc_literals]
221
- @related_specs_heuristic = merged[:related_specs_heuristic]
222
- @fallback_to_full_suite = merged[:fallback_to_full_suite]
223
- @quiet_children = merged[:quiet_children]
224
- @quiet_children_dir = merged[:quiet_children_dir]
235
+ SIMPLE_ATTR_TRANSFORMS.each do |key, transform|
236
+ value = merged[key]
237
+ value = transform.call(value) if transform
238
+ instance_variable_set(:"@#{key}", value)
239
+ end
225
240
  end
226
241
 
242
+ VALIDATED_ATTRS = %i[
243
+ integration jobs fail_fast isolation ignore_patterns
244
+ hooks preload spec_mappings spec_pattern profile
245
+ ].freeze
246
+ private_constant :VALIDATED_ATTRS
247
+
227
248
  def assign_validated_attributes(merged)
228
- @integration = Validators::Integration.call(merged[:integration])
229
- @jobs = Validators::Jobs.call(merged[:jobs])
230
- @fail_fast = Validators::FailFast.call(merged[:fail_fast])
231
- @isolation = Validators::Isolation.call(merged[:isolation])
232
- @ignore_patterns = Validators::IgnorePatterns.call(merged[:ignore_patterns])
233
- @hooks = Validators::Hooks.call(merged[:hooks])
234
- @preload = Validators::Preload.call(merged[:preload])
235
- @spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
236
- @spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
249
+ VALIDATED_ATTRS.each do |key|
250
+ validator = Validators.const_get(key.to_s.split("_").map(&:capitalize).join)
251
+ instance_variable_set(:"@#{key}", validator.call(merged[key]))
252
+ end
237
253
  end
238
254
 
239
255
  def assign_example_targeting(merged)
@@ -259,6 +275,7 @@ require_relative "config/validators/spec_pattern"
259
275
  require_relative "config/validators/spec_mappings"
260
276
  require_relative "config/validators/example_targeting_fallback"
261
277
  require_relative "config/validators/example_targeting_cache"
278
+ require_relative "config/validators/profile"
262
279
  require_relative "config/builders"
263
280
  require_relative "config/builders/spec_resolver"
264
281
  require_relative "config/builders/spec_selector"
@@ -20,21 +20,30 @@ class Evilution::DisableComment
20
20
  private
21
21
 
22
22
  def classify_comments(parse_result, source)
23
- parse_result.comments.filter_map do |comment|
24
- loc = comment.location
25
- text = source.byteslice(loc.start_offset, loc.end_offset - loc.start_offset)
26
- .force_encoding(source.encoding)
27
-
28
- if text.match?(DISABLE_MARKER)
29
- line = source.lines[loc.start_line - 1]
30
- standalone = line.strip == text.strip
31
- { type: :disable, line: loc.start_line, standalone: standalone }
32
- elsif text.match?(ENABLE_MARKER)
33
- { type: :enable, line: loc.start_line }
34
- end
23
+ parse_result.comments.filter_map { |comment| classify_comment(comment, source) }
24
+ end
25
+
26
+ def classify_comment(comment, source)
27
+ loc = comment.location
28
+ text = comment_text(loc, source)
29
+
30
+ if text.match?(DISABLE_MARKER)
31
+ disable_entry(loc, text, source)
32
+ elsif text.match?(ENABLE_MARKER)
33
+ { type: :enable, line: loc.start_line }
35
34
  end
36
35
  end
37
36
 
37
+ def comment_text(loc, source)
38
+ source.byteslice(loc.start_offset, loc.end_offset - loc.start_offset)
39
+ .force_encoding(source.encoding)
40
+ end
41
+
42
+ def disable_entry(loc, text, source)
43
+ standalone = source.lines[loc.start_line - 1].strip == text.strip
44
+ { type: :disable, line: loc.start_line, standalone: standalone }
45
+ end
46
+
38
47
  def scan_comments(comments, method_ranges, total_lines)
39
48
  disabled = []
40
49
  range_start = nil
@@ -26,11 +26,11 @@ class Evilution::Integration::CrashDetector
26
26
  end
27
27
  end
28
28
 
29
- def has_assertion_failure? # rubocop:disable Naming/PredicatePrefix
29
+ def assertion_failure?
30
30
  @assertion_failures.positive?
31
31
  end
32
32
 
33
- def has_crash? # rubocop:disable Naming/PredicatePrefix
33
+ def crashed?
34
34
  @crashes.any?
35
35
  end
36
36