evilution 0.1.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +51 -0
  3. data/.beads/.migration-hint-ts +1 -0
  4. data/.beads/README.md +81 -0
  5. data/.beads/config.yaml +67 -0
  6. data/.beads/interactions.jsonl +0 -0
  7. data/.beads/issues.jsonl +68 -0
  8. data/.beads/metadata.json +4 -0
  9. data/.claude/prompts/architect.md +98 -0
  10. data/.claude/prompts/devops.md +106 -0
  11. data/.claude/prompts/tests.md +160 -0
  12. data/CHANGELOG.md +19 -0
  13. data/CODE_OF_CONDUCT.md +10 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +190 -0
  16. data/Rakefile +12 -0
  17. data/claude-swarm.yml +28 -0
  18. data/exe/evilution +6 -0
  19. data/lib/evilution/ast/parser.rb +83 -0
  20. data/lib/evilution/ast/source_surgeon.rb +13 -0
  21. data/lib/evilution/cli.rb +78 -0
  22. data/lib/evilution/config.rb +98 -0
  23. data/lib/evilution/coverage/collector.rb +47 -0
  24. data/lib/evilution/coverage/test_map.rb +25 -0
  25. data/lib/evilution/diff/file_filter.rb +29 -0
  26. data/lib/evilution/diff/parser.rb +47 -0
  27. data/lib/evilution/integration/base.rb +11 -0
  28. data/lib/evilution/integration/rspec.rb +184 -0
  29. data/lib/evilution/isolation/fork.rb +70 -0
  30. data/lib/evilution/mutation.rb +45 -0
  31. data/lib/evilution/mutator/base.rb +54 -0
  32. data/lib/evilution/mutator/operator/arithmetic_replacement.rb +37 -0
  33. data/lib/evilution/mutator/operator/array_literal.rb +22 -0
  34. data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +31 -0
  35. data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +50 -0
  36. data/lib/evilution/mutator/operator/collection_replacement.rb +37 -0
  37. data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -0
  38. data/lib/evilution/mutator/operator/conditional_branch.rb +36 -0
  39. data/lib/evilution/mutator/operator/conditional_negation.rb +36 -0
  40. data/lib/evilution/mutator/operator/float_literal.rb +26 -0
  41. data/lib/evilution/mutator/operator/hash_literal.rb +22 -0
  42. data/lib/evilution/mutator/operator/integer_literal.rb +45 -0
  43. data/lib/evilution/mutator/operator/method_body_replacement.rb +22 -0
  44. data/lib/evilution/mutator/operator/negation_insertion.rb +22 -0
  45. data/lib/evilution/mutator/operator/nil_replacement.rb +20 -0
  46. data/lib/evilution/mutator/operator/return_value_removal.rb +22 -0
  47. data/lib/evilution/mutator/operator/statement_deletion.rb +24 -0
  48. data/lib/evilution/mutator/operator/string_literal.rb +22 -0
  49. data/lib/evilution/mutator/operator/symbol_literal.rb +20 -0
  50. data/lib/evilution/mutator/registry.rb +55 -0
  51. data/lib/evilution/parallel/pool.rb +98 -0
  52. data/lib/evilution/parallel/worker.rb +24 -0
  53. data/lib/evilution/reporter/cli.rb +72 -0
  54. data/lib/evilution/reporter/json.rb +59 -0
  55. data/lib/evilution/reporter/suggestion.rb +51 -0
  56. data/lib/evilution/result/mutation_result.rb +37 -0
  57. data/lib/evilution/result/summary.rb +54 -0
  58. data/lib/evilution/runner.rb +139 -0
  59. data/lib/evilution/subject.rb +20 -0
  60. data/lib/evilution/version.rb +5 -0
  61. data/lib/evilution.rb +51 -0
  62. data/sig/evilution.rbs +4 -0
  63. metadata +130 -0
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Parallel
5
+ class Pool
6
+ def initialize(jobs:)
7
+ @jobs = jobs
8
+ end
9
+
10
+ # Executes mutations in parallel across N worker processes.
11
+ #
12
+ # @param mutations [Array] Array of mutation objects to run
13
+ # @param test_command_builder [#call] Callable that receives a mutation and returns a test command callable
14
+ # @param timeout [Numeric] Per-mutation timeout in seconds
15
+ # @return [Array<Result::MutationResult>]
16
+ def call(mutations:, test_command_builder:, timeout:)
17
+ return [] if mutations.empty?
18
+
19
+ worker_count = [@jobs, mutations.size].min
20
+ chunks = partition(mutations, worker_count)
21
+
22
+ pipes = worker_count.times.map { IO.pipe }
23
+
24
+ pids = chunks.each_with_index.map do |chunk, index|
25
+ _, write_io = pipes[index]
26
+
27
+ pid = Process.fork do
28
+ # Close all read ends in the child; close sibling write ends too
29
+ pipes.each_with_index do |(r, w), i|
30
+ if i == index
31
+ r.close
32
+ else
33
+ r.close
34
+ w.close
35
+ end
36
+ end
37
+
38
+ results = run_chunk(chunk, test_command_builder, timeout)
39
+ Marshal.dump(results, write_io)
40
+ write_io.close
41
+ exit!(0)
42
+ end
43
+
44
+ write_io.close
45
+ pid
46
+ end
47
+
48
+ results = collect_results(pipes.map(&:first), pids)
49
+
50
+ results
51
+ ensure
52
+ # Ensure all pipes are closed even if something goes wrong
53
+ pipes&.each do |read_io, write_io|
54
+ read_io.close unless read_io.closed?
55
+ write_io.close unless write_io.closed?
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :jobs
62
+
63
+ # Distributes mutations across N chunks using round-robin.
64
+ # File isolation is handled by Integration::RSpec via temp directories
65
+ # and $LOAD_PATH, so same-file mutations can safely run in parallel.
66
+ def partition(mutations, n)
67
+ chunks = Array.new(n) { [] }
68
+ mutations.each_with_index { |m, i| chunks[i % n] << m }
69
+ chunks
70
+ end
71
+
72
+ # Runs a chunk of mutations sequentially inside a worker process.
73
+ def run_chunk(mutations, test_command_builder, timeout)
74
+ worker = Worker.new
75
+ worker.call(mutations: mutations, test_command_builder: test_command_builder, timeout: timeout)
76
+ end
77
+
78
+ # Reads results from all worker pipes and waits for workers to finish.
79
+ def collect_results(read_ios, pids)
80
+ results = []
81
+
82
+ read_ios.each_with_index do |read_io, _index|
83
+ data = read_io.read
84
+ read_io.close
85
+
86
+ unless data.empty?
87
+ chunk_results = Marshal.load(data) # rubocop:disable Security/MarshalLoad
88
+ results.concat(chunk_results)
89
+ end
90
+ end
91
+
92
+ pids.each { |pid| Process.wait(pid) rescue nil } # rubocop:disable Style/RescueModifier
93
+
94
+ results
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Parallel
5
+ class Worker
6
+ def initialize(isolator: Isolation::Fork.new)
7
+ @isolator = isolator
8
+ end
9
+
10
+ # Runs a batch of mutations sequentially using fork isolation.
11
+ #
12
+ # @param mutations [Array<Mutation>] Mutations to execute
13
+ # @param test_command_builder [#call] Receives a mutation, returns a test command callable
14
+ # @param timeout [Numeric] Per-mutation timeout in seconds
15
+ # @return [Array<Result::MutationResult>]
16
+ def call(mutations:, test_command_builder:, timeout:)
17
+ mutations.map do |mutation|
18
+ test_command = test_command_builder.call(mutation)
19
+ @isolator.call(mutation: mutation, test_command: test_command, timeout: timeout)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
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
+
17
+ if summary.survived_results.any?
18
+ lines << ""
19
+ lines << "Survived mutations:"
20
+ summary.survived_results.each do |result|
21
+ lines << format_survived(result)
22
+ end
23
+ end
24
+
25
+ lines << ""
26
+ lines << result_line(summary)
27
+
28
+ lines.join("\n")
29
+ end
30
+
31
+ private
32
+
33
+ def header
34
+ "Evilution v#{Evilution::VERSION} — Mutation Testing Results"
35
+ end
36
+
37
+ def mutations_line(summary)
38
+ "Mutations: #{summary.total} total, #{summary.killed} killed, " \
39
+ "#{summary.survived} survived, #{summary.timed_out} timed out"
40
+ end
41
+
42
+ def score_line(summary)
43
+ denominator = summary.total - summary.errors
44
+ score_pct = format_pct(summary.score)
45
+ "Score: #{score_pct} (#{summary.killed}/#{denominator})"
46
+ end
47
+
48
+ def duration_line(summary)
49
+ "Duration: #{format("%.2f", summary.duration)}s"
50
+ end
51
+
52
+ def format_survived(result)
53
+ mutation = result.mutation
54
+ location = "#{mutation.file_path}:#{mutation.line}"
55
+ diff_lines = mutation.diff.split("\n").map { |l| " #{l}" }.join("\n")
56
+ " #{mutation.operator_name}: #{location}\n#{diff_lines}"
57
+ end
58
+
59
+ def result_line(summary)
60
+ min_score = 0.8
61
+ pass_fail = summary.success?(min_score: min_score) ? "PASS" : "FAIL"
62
+ score_pct = format_pct(summary.score)
63
+ threshold_pct = format_pct(min_score)
64
+ "Result: #{pass_fail} (score #{score_pct} #{pass_fail == "PASS" ? ">=" : "<"} #{threshold_pct})"
65
+ end
66
+
67
+ def format_pct(value)
68
+ format("%.2f%%", value * 100)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "suggestion"
6
+
7
+ module Evilution
8
+ module Reporter
9
+ class JSON
10
+ def initialize
11
+ @suggestion = Suggestion.new
12
+ end
13
+
14
+ def call(summary)
15
+ ::JSON.generate(build_report(summary))
16
+ end
17
+
18
+ private
19
+
20
+ def build_report(summary)
21
+ {
22
+ version: Evilution::VERSION,
23
+ timestamp: Time.now.iso8601,
24
+ summary: build_summary(summary),
25
+ survived: summary.survived_results.map { |r| build_mutation_detail(r) },
26
+ killed: summary.killed_results.map { |r| build_mutation_detail(r) },
27
+ timed_out: summary.results.select(&:timeout?).map { |r| build_mutation_detail(r) },
28
+ errors: summary.results.select(&:error?).map { |r| build_mutation_detail(r) }
29
+ }
30
+ end
31
+
32
+ def build_summary(summary)
33
+ {
34
+ total: summary.total,
35
+ killed: summary.killed,
36
+ survived: summary.survived,
37
+ timed_out: summary.timed_out,
38
+ errors: summary.errors,
39
+ score: summary.score.round(4),
40
+ duration: summary.duration.round(4)
41
+ }
42
+ end
43
+
44
+ def build_mutation_detail(result)
45
+ mutation = result.mutation
46
+ detail = {
47
+ operator: mutation.operator_name,
48
+ file: mutation.file_path,
49
+ line: mutation.line,
50
+ status: result.status.to_s,
51
+ duration: result.duration.round(4),
52
+ diff: mutation.diff
53
+ }
54
+ detail[:suggestion] = @suggestion.suggestion_for(mutation) if result.status == :survived
55
+ detail
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Reporter
5
+ class Suggestion
6
+ TEMPLATES = {
7
+ "comparison_replacement" => "Add a test for the boundary condition where the comparison operand equals the threshold exactly",
8
+ "arithmetic_replacement" => "Add a test that verifies the arithmetic result, not just truthiness of the outcome",
9
+ "boolean_operator_replacement" => "Add a test where only one of the boolean conditions is true to distinguish && from ||",
10
+ "boolean_literal_replacement" => "Add a test that exercises the false/true branch explicitly",
11
+ "nil_replacement" => "Add a test that asserts the return value is not nil",
12
+ "integer_literal" => "Add a test that checks the exact numeric value, not just > 0 or truthy",
13
+ "float_literal" => "Add a test that checks the exact floating-point value returned",
14
+ "string_literal" => "Add a test that asserts the string content, not just its presence",
15
+ "array_literal" => "Add a test that verifies the array contents or length",
16
+ "hash_literal" => "Add a test that verifies the hash keys and values",
17
+ "symbol_literal" => "Add a test that checks the exact symbol returned",
18
+ "conditional_negation" => "Add tests for both the true and false branches of this conditional",
19
+ "conditional_branch" => "Add a test that exercises the removed branch of this conditional",
20
+ "statement_deletion" => "Add a test that depends on the side effect of this statement",
21
+ "method_body_replacement" => "Add a test that checks the method's return value or side effects",
22
+ "negation_insertion" => "Add a test where the predicate result matters (not just truthiness)",
23
+ "return_value_removal" => "Add a test that uses the return value of this method",
24
+ "collection_replacement" => "Add a test that checks the return value of the collection operation, not just side effects"
25
+ }.freeze
26
+
27
+ DEFAULT_SUGGESTION = "Add a more specific test that detects this mutation"
28
+
29
+ # Generate suggestions for survived mutations.
30
+ #
31
+ # @param summary [Result::Summary]
32
+ # @return [Array<Hash>] Array of { mutation:, suggestion: }
33
+ def call(summary)
34
+ summary.survived_results.map do |result|
35
+ {
36
+ mutation: result.mutation,
37
+ suggestion: suggestion_for(result.mutation)
38
+ }
39
+ end
40
+ end
41
+
42
+ # Generate a suggestion for a single mutation.
43
+ #
44
+ # @param mutation [Mutation]
45
+ # @return [String]
46
+ def suggestion_for(mutation)
47
+ TEMPLATES.fetch(mutation.operator_name, DEFAULT_SUGGESTION)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Result
5
+ class MutationResult
6
+ STATUSES = %i[killed survived timeout error].freeze
7
+
8
+ attr_reader :mutation, :status, :duration, :killing_test
9
+
10
+ def initialize(mutation:, status:, duration: 0.0, killing_test: nil)
11
+ raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
12
+
13
+ @mutation = mutation
14
+ @status = status
15
+ @duration = duration
16
+ @killing_test = killing_test
17
+ freeze
18
+ end
19
+
20
+ def killed?
21
+ status == :killed
22
+ end
23
+
24
+ def survived?
25
+ status == :survived
26
+ end
27
+
28
+ def timeout?
29
+ status == :timeout
30
+ end
31
+
32
+ def error?
33
+ status == :error
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ module Result
5
+ class Summary
6
+ attr_reader :results, :duration
7
+
8
+ def initialize(results:, duration: 0.0)
9
+ @results = results
10
+ @duration = duration
11
+ freeze
12
+ end
13
+
14
+ def total
15
+ results.length
16
+ end
17
+
18
+ def killed
19
+ results.count(&:killed?)
20
+ end
21
+
22
+ def survived
23
+ results.count(&:survived?)
24
+ end
25
+
26
+ def timed_out
27
+ results.count(&:timeout?)
28
+ end
29
+
30
+ def errors
31
+ results.count(&:error?)
32
+ end
33
+
34
+ def score
35
+ denominator = total - errors
36
+ return 0.0 if denominator.zero?
37
+
38
+ killed.to_f / denominator
39
+ end
40
+
41
+ def success?(min_score: 1.0)
42
+ score >= min_score
43
+ end
44
+
45
+ def survived_results
46
+ results.select(&:survived?)
47
+ end
48
+
49
+ def killed_results
50
+ results.select(&:killed?)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config"
4
+ require_relative "ast/parser"
5
+ require_relative "mutator/registry"
6
+ require_relative "isolation/fork"
7
+ require_relative "parallel/pool"
8
+ require_relative "integration/rspec"
9
+ require_relative "reporter/json"
10
+ require_relative "reporter/cli"
11
+ require_relative "reporter/suggestion"
12
+ require_relative "coverage/collector"
13
+ require_relative "coverage/test_map"
14
+ require_relative "diff/parser"
15
+ require_relative "diff/file_filter"
16
+ require_relative "result/mutation_result"
17
+ require_relative "result/summary"
18
+
19
+ module Evilution
20
+ class Runner
21
+ attr_reader :config
22
+
23
+ def initialize(config: Config.new)
24
+ @config = config
25
+ @parser = AST::Parser.new
26
+ @registry = Mutator::Registry.default
27
+ @isolator = Isolation::Fork.new
28
+ end
29
+
30
+ def call
31
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+
33
+ subjects = parse_subjects
34
+ subjects = filter_by_diff(subjects) if config.diff?
35
+ mutations = generate_mutations(subjects)
36
+ test_map = collect_coverage if config.coverage && config.integration == :rspec
37
+ mutations, skipped = filter_by_coverage(mutations, test_map) if test_map
38
+ results = run_mutations(mutations)
39
+ results.concat(skipped) if skipped
40
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
41
+
42
+ summary = Result::Summary.new(results: results, duration: duration)
43
+ output_report(summary)
44
+
45
+ summary
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :parser, :registry, :isolator
51
+
52
+ def parse_subjects
53
+ config.target_files.flat_map { |file| parser.call(file) }
54
+ end
55
+
56
+ def filter_by_diff(subjects)
57
+ diff_parser = Diff::Parser.new
58
+ changed_ranges = diff_parser.parse(config.diff_base)
59
+ Diff::FileFilter.new.filter(subjects, changed_ranges)
60
+ end
61
+
62
+ def generate_mutations(subjects)
63
+ subjects.flat_map { |subject| registry.mutations_for(subject) }
64
+ end
65
+
66
+ def collect_coverage
67
+ test_files = Dir.glob("spec/**/*_spec.rb")
68
+ return nil if test_files.empty?
69
+
70
+ data = Coverage::Collector.new.call(test_files: test_files)
71
+ Coverage::TestMap.new(data)
72
+ end
73
+
74
+ def filter_by_coverage(mutations, test_map)
75
+ covered, uncovered = mutations.partition do |m|
76
+ test_map.covered?(File.expand_path(m.file_path), m.line)
77
+ end
78
+
79
+ skipped = uncovered.map do |m|
80
+ Result::MutationResult.new(mutation: m, status: :survived, duration: 0.0)
81
+ end
82
+
83
+ [covered, skipped]
84
+ end
85
+
86
+ def run_mutations(mutations)
87
+ integration = build_integration
88
+
89
+ if config.jobs > 1 && mutations.size > 1
90
+ run_parallel(mutations, integration)
91
+ else
92
+ run_sequential(mutations, integration)
93
+ end
94
+ end
95
+
96
+ def run_sequential(mutations, integration)
97
+ mutations.map do |mutation|
98
+ test_command = ->(m) { integration.call(m) }
99
+ isolator.call(
100
+ mutation: mutation,
101
+ test_command: test_command,
102
+ timeout: config.timeout
103
+ )
104
+ end
105
+ end
106
+
107
+ def run_parallel(mutations, integration)
108
+ pool = Parallel::Pool.new(jobs: config.jobs)
109
+ test_command_builder = ->(_mutation) { ->(m) { integration.call(m) } }
110
+ pool.call(mutations: mutations, test_command_builder: test_command_builder, timeout: config.timeout)
111
+ end
112
+
113
+ def build_integration
114
+ case config.integration
115
+ when :rspec
116
+ Integration::RSpec.new
117
+ else
118
+ raise Error, "unknown integration: #{config.integration}"
119
+ end
120
+ end
121
+
122
+ def output_report(summary)
123
+ reporter = build_reporter
124
+ return unless reporter
125
+
126
+ output = reporter.call(summary)
127
+ $stdout.puts(output) unless config.quiet
128
+ end
129
+
130
+ def build_reporter
131
+ case config.format
132
+ when :json
133
+ Reporter::JSON.new
134
+ when :text
135
+ Reporter::CLI.new
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ class Subject
5
+ attr_reader :name, :file_path, :line_number, :source, :node
6
+
7
+ def initialize(name:, file_path:, line_number:, source:, node:)
8
+ @name = name
9
+ @file_path = file_path
10
+ @line_number = line_number
11
+ @source = source
12
+ @node = node
13
+ freeze
14
+ end
15
+
16
+ def to_s
17
+ "#{name} (#{file_path}:#{line_number})"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evilution
4
+ VERSION = "0.1.0"
5
+ end
data/lib/evilution.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "evilution/version"
4
+ require_relative "evilution/config"
5
+ require_relative "evilution/subject"
6
+ require_relative "evilution/mutation"
7
+ require_relative "evilution/ast/source_surgeon"
8
+ require_relative "evilution/ast/parser"
9
+ require_relative "evilution/mutator/base"
10
+ require_relative "evilution/mutator/operator/comparison_replacement"
11
+ require_relative "evilution/mutator/operator/boolean_literal_replacement"
12
+ require_relative "evilution/mutator/operator/integer_literal"
13
+ require_relative "evilution/mutator/operator/float_literal"
14
+ require_relative "evilution/mutator/operator/nil_replacement"
15
+ require_relative "evilution/mutator/operator/boolean_operator_replacement"
16
+ require_relative "evilution/mutator/operator/arithmetic_replacement"
17
+ require_relative "evilution/mutator/operator/string_literal"
18
+ require_relative "evilution/mutator/operator/array_literal"
19
+ require_relative "evilution/mutator/operator/hash_literal"
20
+ require_relative "evilution/mutator/operator/conditional_branch"
21
+ require_relative "evilution/mutator/operator/symbol_literal"
22
+ require_relative "evilution/mutator/operator/conditional_negation"
23
+ require_relative "evilution/mutator/operator/negation_insertion"
24
+ require_relative "evilution/mutator/operator/statement_deletion"
25
+ require_relative "evilution/mutator/operator/method_body_replacement"
26
+ require_relative "evilution/mutator/operator/return_value_removal"
27
+ require_relative "evilution/mutator/operator/collection_replacement"
28
+ require_relative "evilution/mutator/registry"
29
+ require_relative "evilution/isolation/fork"
30
+ require_relative "evilution/parallel/worker"
31
+ require_relative "evilution/parallel/pool"
32
+ require_relative "evilution/diff/parser"
33
+ require_relative "evilution/diff/file_filter"
34
+ require_relative "evilution/integration/base"
35
+ require_relative "evilution/integration/rspec"
36
+ require_relative "evilution/result/mutation_result"
37
+ require_relative "evilution/result/summary"
38
+ require_relative "evilution/reporter/json"
39
+ require_relative "evilution/reporter/cli"
40
+ require_relative "evilution/reporter/suggestion"
41
+ require_relative "evilution/coverage/collector"
42
+ require_relative "evilution/coverage/test_map"
43
+ require_relative "evilution/cli"
44
+ require_relative "evilution/runner"
45
+
46
+ module Evilution
47
+ class Error < StandardError; end
48
+ class ConfigError < Error; end
49
+ class ParseError < Error; end
50
+ class IsolationError < Error; end
51
+ end
data/sig/evilution.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Evilution
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end