evilution 0.13.0 → 0.14.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +8 -8
  4. data/CHANGELOG.md +17 -0
  5. data/lib/evilution/ast/parser.rb +69 -68
  6. data/lib/evilution/ast/source_surgeon.rb +7 -9
  7. data/lib/evilution/ast.rb +4 -0
  8. data/lib/evilution/baseline.rb +73 -75
  9. data/lib/evilution/cache.rb +75 -77
  10. data/lib/evilution/cli.rb +408 -173
  11. data/lib/evilution/config.rb +141 -136
  12. data/lib/evilution/equivalent/detector.rb +25 -27
  13. data/lib/evilution/equivalent/heuristic/alias_swap.rb +29 -33
  14. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  15. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  16. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  17. data/lib/evilution/equivalent/heuristic.rb +6 -0
  18. data/lib/evilution/equivalent.rb +4 -0
  19. data/lib/evilution/git/changed_files.rb +35 -37
  20. data/lib/evilution/git.rb +4 -0
  21. data/lib/evilution/integration/base.rb +5 -7
  22. data/lib/evilution/integration/rspec.rb +114 -116
  23. data/lib/evilution/integration.rb +4 -0
  24. data/lib/evilution/isolation/fork.rb +98 -100
  25. data/lib/evilution/isolation/in_process.rb +59 -61
  26. data/lib/evilution/isolation.rb +4 -0
  27. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  28. data/lib/evilution/mcp/server.rb +12 -11
  29. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  30. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  31. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  32. data/lib/evilution/mcp.rb +4 -0
  33. data/lib/evilution/memory/leak_check.rb +80 -84
  34. data/lib/evilution/memory.rb +34 -36
  35. data/lib/evilution/mutation.rb +40 -42
  36. data/lib/evilution/mutator/base.rb +46 -48
  37. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  38. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  39. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  40. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  41. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  42. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  43. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  44. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  45. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  46. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  47. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  48. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  49. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  50. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  51. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  52. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  53. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  54. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  55. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  56. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  57. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  58. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  59. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  60. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  61. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  62. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  63. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  64. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  65. data/lib/evilution/mutator/operator.rb +6 -0
  66. data/lib/evilution/mutator/registry.rb +54 -56
  67. data/lib/evilution/mutator.rb +4 -0
  68. data/lib/evilution/parallel/pool.rb +56 -58
  69. data/lib/evilution/parallel.rb +4 -0
  70. data/lib/evilution/reporter/cli.rb +99 -101
  71. data/lib/evilution/reporter/html.rb +242 -244
  72. data/lib/evilution/reporter/json.rb +57 -59
  73. data/lib/evilution/reporter/suggestion.rb +326 -328
  74. data/lib/evilution/reporter.rb +4 -0
  75. data/lib/evilution/result/mutation_result.rb +43 -46
  76. data/lib/evilution/result/summary.rb +80 -81
  77. data/lib/evilution/result.rb +4 -0
  78. data/lib/evilution/runner.rb +334 -323
  79. data/lib/evilution/session/store.rb +147 -0
  80. data/lib/evilution/session.rb +4 -0
  81. data/lib/evilution/spec_resolver.rb +49 -47
  82. data/lib/evilution/subject.rb +14 -16
  83. data/lib/evilution/version.rb +1 -1
  84. data/lib/evilution.rb +13 -0
  85. metadata +19 -2
@@ -1,105 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Evilution
4
- module Reporter
5
- class CLI
6
- SEPARATOR = "=" * 44
7
-
8
- def call(summary)
9
- lines = []
10
- lines << header
11
- lines << SEPARATOR
12
- lines << ""
13
- lines << mutations_line(summary)
14
- lines << score_line(summary)
15
- lines << duration_line(summary)
16
- peak = summary.peak_memory_mb
17
- lines << peak_memory_line(peak) if peak
18
- append_survived(lines, summary)
19
- append_neutral(lines, summary)
20
- append_equivalent(lines, summary)
21
- lines << ""
22
- lines << "[TRUNCATED] Stopped early due to --fail-fast" if summary.truncated?
23
- lines << result_line(summary)
24
-
25
- lines.join("\n")
26
- end
27
-
28
- private
29
-
30
- def append_survived(lines, summary)
31
- return unless summary.survived_results.any?
32
-
33
- lines << ""
34
- lines << "Survived mutations:"
35
- summary.survived_results.each { |result| lines << format_survived(result) }
36
- end
37
-
38
- def append_neutral(lines, summary)
39
- return unless summary.neutral_results.any?
40
-
41
- lines << ""
42
- lines << "Neutral mutations (test already failing):"
43
- summary.neutral_results.each { |result| lines << format_neutral(result) }
44
- end
45
-
46
- def append_equivalent(lines, summary)
47
- return unless summary.equivalent_results.any?
48
-
49
- lines << ""
50
- lines << "Equivalent mutations (provably identical behavior):"
51
- summary.equivalent_results.each { |result| lines << format_neutral(result) }
52
- end
53
-
54
- def header
55
- "Evilution v#{Evilution::VERSION} — Mutation Testing Results"
56
- end
57
-
58
- def mutations_line(summary)
59
- parts = "Mutations: #{summary.total} total, #{summary.killed} killed, " \
60
- "#{summary.survived} survived, #{summary.timed_out} timed out"
61
- parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
62
- parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
63
- parts
64
- end
65
-
66
- def score_line(summary)
67
- denominator = summary.total - summary.errors - summary.neutral - summary.equivalent
68
- score_pct = format_pct(summary.score)
69
- "Score: #{score_pct} (#{summary.killed}/#{denominator})"
70
- end
71
-
72
- def duration_line(summary)
73
- "Duration: #{format("%.2f", summary.duration)}s"
74
- end
75
-
76
- def format_survived(result)
77
- mutation = result.mutation
78
- location = "#{mutation.file_path}:#{mutation.line}"
79
- diff_lines = mutation.diff.split("\n").map { |l| " #{l}" }.join("\n")
80
- " #{mutation.operator_name}: #{location}\n#{diff_lines}"
81
- end
82
-
83
- def format_neutral(result)
84
- mutation = result.mutation
85
- " #{mutation.operator_name}: #{mutation.file_path}:#{mutation.line}"
86
- end
87
-
88
- def result_line(summary)
89
- min_score = 0.8
90
- pass_fail = summary.success?(min_score: min_score) ? "PASS" : "FAIL"
91
- score_pct = format_pct(summary.score)
92
- threshold_pct = format_pct(min_score)
93
- "Result: #{pass_fail} (score #{score_pct} #{pass_fail == "PASS" ? ">=" : "<"} #{threshold_pct})"
94
- end
95
-
96
- def peak_memory_line(peak_mb)
97
- format("Peak memory: %<mb>.1f MB", mb: peak_mb)
98
- end
99
-
100
- def format_pct(value)
101
- format("%.2f%%", value * 100)
102
- end
103
- end
3
+ require_relative "../reporter"
4
+
5
+ class Evilution::Reporter::CLI
6
+ SEPARATOR = "=" * 44
7
+
8
+ def call(summary)
9
+ lines = []
10
+ lines << header
11
+ lines << SEPARATOR
12
+ lines << ""
13
+ lines << mutations_line(summary)
14
+ lines << score_line(summary)
15
+ lines << duration_line(summary)
16
+ peak = summary.peak_memory_mb
17
+ lines << peak_memory_line(peak) if peak
18
+ append_survived(lines, summary)
19
+ append_neutral(lines, summary)
20
+ append_equivalent(lines, summary)
21
+ lines << ""
22
+ lines << "[TRUNCATED] Stopped early due to --fail-fast" if summary.truncated?
23
+ lines << result_line(summary)
24
+
25
+ lines.join("\n")
26
+ end
27
+
28
+ private
29
+
30
+ def append_survived(lines, summary)
31
+ return unless summary.survived_results.any?
32
+
33
+ lines << ""
34
+ lines << "Survived mutations:"
35
+ summary.survived_results.each { |result| lines << format_survived(result) }
36
+ end
37
+
38
+ def append_neutral(lines, summary)
39
+ return unless summary.neutral_results.any?
40
+
41
+ lines << ""
42
+ lines << "Neutral mutations (test already failing):"
43
+ summary.neutral_results.each { |result| lines << format_neutral(result) }
44
+ end
45
+
46
+ def append_equivalent(lines, summary)
47
+ return unless summary.equivalent_results.any?
48
+
49
+ lines << ""
50
+ lines << "Equivalent mutations (provably identical behavior):"
51
+ summary.equivalent_results.each { |result| lines << format_neutral(result) }
52
+ end
53
+
54
+ def header
55
+ "Evilution v#{Evilution::VERSION} — Mutation Testing Results"
56
+ end
57
+
58
+ def mutations_line(summary)
59
+ parts = "Mutations: #{summary.total} total, #{summary.killed} killed, " \
60
+ "#{summary.survived} survived, #{summary.timed_out} timed out"
61
+ parts += ", #{summary.neutral} neutral" if summary.neutral.positive?
62
+ parts += ", #{summary.equivalent} equivalent" if summary.equivalent.positive?
63
+ parts
64
+ end
65
+
66
+ def score_line(summary)
67
+ denominator = summary.total - summary.errors - summary.neutral - summary.equivalent
68
+ score_pct = format_pct(summary.score)
69
+ "Score: #{score_pct} (#{summary.killed}/#{denominator})"
70
+ end
71
+
72
+ def duration_line(summary)
73
+ "Duration: #{format("%.2f", summary.duration)}s"
74
+ end
75
+
76
+ def format_survived(result)
77
+ mutation = result.mutation
78
+ location = "#{mutation.file_path}:#{mutation.line}"
79
+ diff_lines = mutation.diff.split("\n").map { |l| " #{l}" }.join("\n")
80
+ " #{mutation.operator_name}: #{location}\n#{diff_lines}"
81
+ end
82
+
83
+ def format_neutral(result)
84
+ mutation = result.mutation
85
+ " #{mutation.operator_name}: #{mutation.file_path}:#{mutation.line}"
86
+ end
87
+
88
+ def result_line(summary)
89
+ min_score = 0.8
90
+ pass_fail = summary.success?(min_score: min_score) ? "PASS" : "FAIL"
91
+ score_pct = format_pct(summary.score)
92
+ threshold_pct = format_pct(min_score)
93
+ "Result: #{pass_fail} (score #{score_pct} #{pass_fail == "PASS" ? ">=" : "<"} #{threshold_pct})"
94
+ end
95
+
96
+ def peak_memory_line(peak_mb)
97
+ format("Peak memory: %<mb>.1f MB", mb: peak_mb)
98
+ end
99
+
100
+ def format_pct(value)
101
+ format("%.2f%%", value * 100)
104
102
  end
105
103
  end