evilution 0.13.0 → 0.15.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.migration-hint-ts +1 -1
  3. data/.beads/issues.jsonl +17 -17
  4. data/CHANGELOG.md +39 -0
  5. data/lib/evilution/ast/inheritance_scanner.rb +70 -0
  6. data/lib/evilution/ast/parser.rb +73 -68
  7. data/lib/evilution/ast/source_surgeon.rb +7 -9
  8. data/lib/evilution/ast.rb +4 -0
  9. data/lib/evilution/baseline.rb +73 -75
  10. data/lib/evilution/cache.rb +75 -77
  11. data/lib/evilution/cli.rb +412 -173
  12. data/lib/evilution/config.rb +141 -136
  13. data/lib/evilution/equivalent/detector.rb +29 -27
  14. data/lib/evilution/equivalent/heuristic/alias_swap.rb +32 -33
  15. data/lib/evilution/equivalent/heuristic/arithmetic_identity.rb +30 -0
  16. data/lib/evilution/equivalent/heuristic/comment_marking.rb +21 -0
  17. data/lib/evilution/equivalent/heuristic/dead_code.rb +41 -45
  18. data/lib/evilution/equivalent/heuristic/method_body_nil.rb +11 -15
  19. data/lib/evilution/equivalent/heuristic/noop_source.rb +5 -9
  20. data/lib/evilution/equivalent/heuristic.rb +6 -0
  21. data/lib/evilution/equivalent.rb +4 -0
  22. data/lib/evilution/git/changed_files.rb +35 -37
  23. data/lib/evilution/git.rb +4 -0
  24. data/lib/evilution/integration/base.rb +5 -7
  25. data/lib/evilution/integration/rspec.rb +114 -116
  26. data/lib/evilution/integration.rb +4 -0
  27. data/lib/evilution/isolation/fork.rb +98 -100
  28. data/lib/evilution/isolation/in_process.rb +59 -61
  29. data/lib/evilution/isolation.rb +4 -0
  30. data/lib/evilution/mcp/mutate_tool.rb +172 -143
  31. data/lib/evilution/mcp/server.rb +12 -11
  32. data/lib/evilution/mcp/session_diff_tool.rb +89 -0
  33. data/lib/evilution/mcp/session_list_tool.rb +46 -0
  34. data/lib/evilution/mcp/session_show_tool.rb +53 -0
  35. data/lib/evilution/mcp.rb +4 -0
  36. data/lib/evilution/memory/leak_check.rb +80 -84
  37. data/lib/evilution/memory.rb +34 -36
  38. data/lib/evilution/mutation.rb +40 -42
  39. data/lib/evilution/mutator/base.rb +62 -48
  40. data/lib/evilution/mutator/operator/argument_nil_substitution.rb +32 -36
  41. data/lib/evilution/mutator/operator/argument_removal.rb +32 -36
  42. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +26 -30
  43. data/lib/evilution/mutator/operator/array_literal.rb +18 -22
  44. data/lib/evilution/mutator/operator/block_removal.rb +16 -20
  45. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +38 -42
  46. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +41 -45
  47. data/lib/evilution/mutator/operator/collection_replacement.rb +32 -36
  48. data/lib/evilution/mutator/operator/comparison_replacement.rb +24 -28
  49. data/lib/evilution/mutator/operator/compound_assignment.rb +114 -118
  50. data/lib/evilution/mutator/operator/conditional_branch.rb +25 -29
  51. data/lib/evilution/mutator/operator/conditional_flip.rb +26 -30
  52. data/lib/evilution/mutator/operator/conditional_negation.rb +25 -29
  53. data/lib/evilution/mutator/operator/float_literal.rb +22 -26
  54. data/lib/evilution/mutator/operator/hash_literal.rb +18 -22
  55. data/lib/evilution/mutator/operator/integer_literal.rb +2 -0
  56. data/lib/evilution/mutator/operator/method_body_replacement.rb +12 -16
  57. data/lib/evilution/mutator/operator/method_call_removal.rb +12 -16
  58. data/lib/evilution/mutator/operator/mixin_removal.rb +80 -0
  59. data/lib/evilution/mutator/operator/negation_insertion.rb +12 -16
  60. data/lib/evilution/mutator/operator/nil_replacement.rb +13 -17
  61. data/lib/evilution/mutator/operator/range_replacement.rb +12 -16
  62. data/lib/evilution/mutator/operator/receiver_replacement.rb +16 -20
  63. data/lib/evilution/mutator/operator/regexp_mutation.rb +15 -19
  64. data/lib/evilution/mutator/operator/return_value_removal.rb +12 -16
  65. data/lib/evilution/mutator/operator/send_mutation.rb +36 -40
  66. data/lib/evilution/mutator/operator/statement_deletion.rb +13 -17
  67. data/lib/evilution/mutator/operator/string_literal.rb +18 -22
  68. data/lib/evilution/mutator/operator/superclass_removal.rb +65 -0
  69. data/lib/evilution/mutator/operator/symbol_literal.rb +17 -21
  70. data/lib/evilution/mutator/operator.rb +6 -0
  71. data/lib/evilution/mutator/registry.rb +56 -56
  72. data/lib/evilution/mutator.rb +4 -0
  73. data/lib/evilution/parallel/pool.rb +56 -58
  74. data/lib/evilution/parallel.rb +4 -0
  75. data/lib/evilution/reporter/cli.rb +99 -101
  76. data/lib/evilution/reporter/html.rb +242 -244
  77. data/lib/evilution/reporter/json.rb +57 -59
  78. data/lib/evilution/reporter/suggestion.rb +354 -328
  79. data/lib/evilution/reporter.rb +4 -0
  80. data/lib/evilution/result/mutation_result.rb +43 -46
  81. data/lib/evilution/result/summary.rb +80 -81
  82. data/lib/evilution/result.rb +4 -0
  83. data/lib/evilution/runner.rb +401 -316
  84. data/lib/evilution/session/store.rb +147 -0
  85. data/lib/evilution/session.rb +4 -0
  86. data/lib/evilution/spec_resolver.rb +49 -47
  87. data/lib/evilution/subject.rb +14 -16
  88. data/lib/evilution/version.rb +1 -1
  89. data/lib/evilution.rb +16 -0
  90. metadata +24 -2
@@ -2,99 +2,97 @@
2
2
 
3
3
  require_relative "spec_resolver"
4
4
 
5
- module Evilution
6
- class Baseline
7
- Result = Struct.new(:failed_spec_files, :duration) do
8
- def initialize(**)
9
- super
10
- freeze
11
- end
12
-
13
- def failed?
14
- !failed_spec_files.empty?
15
- end
5
+ class Evilution::Baseline
6
+ Result = Struct.new(:failed_spec_files, :duration) do
7
+ def initialize(**)
8
+ super
9
+ freeze
16
10
  end
17
11
 
18
- def initialize(spec_resolver: SpecResolver.new, timeout: 30)
19
- @spec_resolver = spec_resolver
20
- @timeout = timeout
12
+ def failed?
13
+ !failed_spec_files.empty?
21
14
  end
15
+ end
22
16
 
23
- def call(subjects)
24
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
25
- spec_files = resolve_unique_spec_files(subjects)
26
- failed = Set.new
17
+ def initialize(spec_resolver: Evilution::SpecResolver.new, timeout: 30)
18
+ @spec_resolver = spec_resolver
19
+ @timeout = timeout
20
+ end
27
21
 
28
- spec_files.each do |spec_file|
29
- failed.add(spec_file) unless run_spec_file(spec_file)
30
- end
22
+ def call(subjects)
23
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ spec_files = resolve_unique_spec_files(subjects)
25
+ failed = Set.new
31
26
 
32
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
33
- Result.new(failed_spec_files: failed, duration: duration)
27
+ spec_files.each do |spec_file|
28
+ failed.add(spec_file) unless run_spec_file(spec_file)
34
29
  end
35
30
 
36
- def run_spec_file(spec_file)
37
- read_io, write_io = IO.pipe
38
- pid = fork_spec_runner(spec_file, read_io, write_io)
39
- write_io.close
40
- read_result(read_io, pid)
41
- rescue StandardError
42
- false
43
- ensure
44
- read_io&.close
45
- write_io&.close
46
- end
31
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
32
+ Result.new(failed_spec_files: failed, duration: duration)
33
+ end
34
+
35
+ def run_spec_file(spec_file)
36
+ read_io, write_io = IO.pipe
37
+ pid = fork_spec_runner(spec_file, read_io, write_io)
38
+ write_io.close
39
+ read_result(read_io, pid)
40
+ rescue StandardError
41
+ false
42
+ ensure
43
+ read_io&.close
44
+ write_io&.close
45
+ end
47
46
 
48
- def fork_spec_runner(spec_file, read_io, write_io)
49
- Process.fork do
50
- read_io.close
51
- $stdout.reopen(File::NULL, "w")
52
- $stderr.reopen(File::NULL, "w")
53
-
54
- require "rspec/core"
55
- ::RSpec.reset
56
- status = ::RSpec::Core::Runner.run(
57
- ["--format", "progress", "--no-color", "--order", "defined", spec_file]
58
- )
59
- Marshal.dump({ passed: status.zero? }, write_io)
60
- write_io.close
61
- exit!(status.zero? ? 0 : 1)
62
- end
47
+ def fork_spec_runner(spec_file, read_io, write_io)
48
+ Process.fork do
49
+ read_io.close
50
+ $stdout.reopen(File::NULL, "w")
51
+ $stderr.reopen(File::NULL, "w")
52
+
53
+ require "rspec/core"
54
+ RSpec.reset
55
+ status = RSpec::Core::Runner.run(
56
+ ["--format", "progress", "--no-color", "--order", "defined", spec_file]
57
+ )
58
+ Marshal.dump({ passed: status.zero? }, write_io)
59
+ write_io.close
60
+ exit!(status.zero? ? 0 : 1)
63
61
  end
62
+ end
64
63
 
65
- GRACE_PERIOD = 0.5
64
+ GRACE_PERIOD = 0.5
66
65
 
67
- def read_result(read_io, pid)
68
- if read_io.wait_readable(@timeout)
69
- data = read_io.read
70
- Process.wait(pid)
71
- return false if data.empty?
66
+ def read_result(read_io, pid)
67
+ if read_io.wait_readable(@timeout)
68
+ data = read_io.read
69
+ Process.wait(pid)
70
+ return false if data.empty?
72
71
 
73
- result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
74
- result[:passed]
75
- else
76
- terminate_child(pid)
77
- false
78
- end
72
+ result = Marshal.load(data) # rubocop:disable Security/MarshalLoad
73
+ result[:passed]
74
+ else
75
+ terminate_child(pid)
76
+ false
79
77
  end
78
+ end
80
79
 
81
- def terminate_child(pid)
82
- Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
83
- _, status = Process.waitpid2(pid, Process::WNOHANG)
84
- return if status
80
+ def terminate_child(pid)
81
+ Process.kill("TERM", pid) rescue nil # rubocop:disable Style/RescueModifier
82
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
83
+ return if status
85
84
 
86
- sleep(GRACE_PERIOD)
87
- _, status = Process.waitpid2(pid, Process::WNOHANG)
88
- return if status
85
+ sleep(GRACE_PERIOD)
86
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
87
+ return if status
89
88
 
90
- Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
91
- Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
92
- end
89
+ Process.kill("KILL", pid) rescue nil # rubocop:disable Style/RescueModifier
90
+ Process.wait(pid) rescue nil # rubocop:disable Style/RescueModifier
91
+ end
93
92
 
94
- private
93
+ private
95
94
 
96
- def resolve_unique_spec_files(subjects)
97
- subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
98
- end
95
+ def resolve_unique_spec_files(subjects)
96
+ subjects.map { |s| @spec_resolver.call(s.file_path) || "spec" }.uniq
99
97
  end
100
98
  end
@@ -4,82 +4,80 @@ require "digest"
4
4
  require "json"
5
5
  require "fileutils"
6
6
 
7
- module Evilution
8
- class Cache
9
- DEFAULT_DIR = "tmp/evilution_cache"
10
-
11
- def initialize(cache_dir: DEFAULT_DIR)
12
- @cache_dir = cache_dir
13
- end
14
-
15
- def fetch(mutation)
16
- return nil if mutation.original_source.nil?
17
-
18
- file_key = file_key(mutation)
19
- entry_key = entry_key(mutation)
20
- data = read_file(file_key)
21
- return nil unless data
22
-
23
- entry = data[entry_key]
24
- return nil unless entry.is_a?(Hash) && entry["status"].is_a?(String)
25
-
26
- { status: entry["status"].to_sym, duration: entry["duration"],
27
- killing_test: entry["killing_test"], test_command: entry["test_command"] }
28
- end
29
-
30
- def store(mutation, result_data)
31
- file_key = file_key(mutation)
32
- entry_key = entry_key(mutation)
33
- data = read_file(file_key) || {}
34
-
35
- data[entry_key] = {
36
- "status" => result_data[:status].to_s,
37
- "duration" => result_data[:duration],
38
- "killing_test" => result_data[:killing_test],
39
- "test_command" => result_data[:test_command]
40
- }
41
-
42
- write_file(file_key, data)
43
- end
44
-
45
- def clear
46
- FileUtils.rm_rf(@cache_dir)
47
- end
48
-
49
- private
50
-
51
- def file_key(mutation)
52
- content_hash = Digest::SHA256.hexdigest(mutation.original_source)
53
- "#{safe_filename(mutation.file_path)}_#{content_hash[0, 16]}"
54
- end
55
-
56
- def entry_key(mutation)
57
- "#{mutation.operator_name}:#{mutation.line}:#{mutation.column}"
58
- end
59
-
60
- def safe_filename(path)
61
- path.gsub(%r{[/\\]}, "_").gsub(/[^a-zA-Z0-9._-]/, "")
62
- end
63
-
64
- def read_file(file_key)
65
- path = cache_path(file_key)
66
- return nil unless File.exist?(path)
67
-
68
- JSON.parse(File.read(path))
69
- rescue JSON::ParserError
70
- nil
71
- end
72
-
73
- def write_file(file_key, data)
74
- FileUtils.mkdir_p(@cache_dir)
75
- path = cache_path(file_key)
76
- tmp = "#{path}.#{Process.pid}.tmp"
77
- File.write(tmp, JSON.generate(data))
78
- File.rename(tmp, path)
79
- end
80
-
81
- def cache_path(file_key)
82
- File.join(@cache_dir, "#{file_key}.json")
83
- end
7
+ class Evilution::Cache
8
+ DEFAULT_DIR = "tmp/evilution_cache"
9
+
10
+ def initialize(cache_dir: DEFAULT_DIR)
11
+ @cache_dir = cache_dir
12
+ end
13
+
14
+ def fetch(mutation)
15
+ return nil if mutation.original_source.nil?
16
+
17
+ file_key = file_key(mutation)
18
+ entry_key = entry_key(mutation)
19
+ data = read_file(file_key)
20
+ return nil unless data
21
+
22
+ entry = data[entry_key]
23
+ return nil unless entry.is_a?(Hash) && entry["status"].is_a?(String)
24
+
25
+ { status: entry["status"].to_sym, duration: entry["duration"],
26
+ killing_test: entry["killing_test"], test_command: entry["test_command"] }
27
+ end
28
+
29
+ def store(mutation, result_data)
30
+ file_key = file_key(mutation)
31
+ entry_key = entry_key(mutation)
32
+ data = read_file(file_key) || {}
33
+
34
+ data[entry_key] = {
35
+ "status" => result_data[:status].to_s,
36
+ "duration" => result_data[:duration],
37
+ "killing_test" => result_data[:killing_test],
38
+ "test_command" => result_data[:test_command]
39
+ }
40
+
41
+ write_file(file_key, data)
42
+ end
43
+
44
+ def clear
45
+ FileUtils.rm_rf(@cache_dir)
46
+ end
47
+
48
+ private
49
+
50
+ def file_key(mutation)
51
+ content_hash = Digest::SHA256.hexdigest(mutation.original_source)
52
+ "#{safe_filename(mutation.file_path)}_#{content_hash[0, 16]}"
53
+ end
54
+
55
+ def entry_key(mutation)
56
+ "#{mutation.operator_name}:#{mutation.line}:#{mutation.column}"
57
+ end
58
+
59
+ def safe_filename(path)
60
+ path.gsub(%r{[/\\]}, "_").gsub(/[^a-zA-Z0-9._-]/, "")
61
+ end
62
+
63
+ def read_file(file_key)
64
+ path = cache_path(file_key)
65
+ return nil unless File.exist?(path)
66
+
67
+ JSON.parse(File.read(path))
68
+ rescue JSON::ParserError
69
+ nil
70
+ end
71
+
72
+ def write_file(file_key, data)
73
+ FileUtils.mkdir_p(@cache_dir)
74
+ path = cache_path(file_key)
75
+ tmp = "#{path}.#{Process.pid}.tmp"
76
+ File.write(tmp, JSON.generate(data))
77
+ File.rename(tmp, path)
78
+ end
79
+
80
+ def cache_path(file_key)
81
+ File.join(@cache_dir, "#{file_key}.json")
84
82
  end
85
83
  end