mutineer 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +42 -0
- data/LICENSE +21 -0
- data/README.md +118 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/cli.rb +261 -0
- data/lib/mutineer/config.rb +145 -0
- data/lib/mutineer/coverage_map.rb +222 -0
- data/lib/mutineer/isolation.rb +157 -0
- data/lib/mutineer/minitest_integration.rb +43 -0
- data/lib/mutineer/mutation.rb +21 -0
- data/lib/mutineer/mutator_registry.rb +54 -0
- data/lib/mutineer/mutators/arithmetic.rb +29 -0
- data/lib/mutineer/mutators/base.rb +23 -0
- data/lib/mutineer/mutators/boolean_connector.rb +42 -0
- data/lib/mutineer/mutators/boolean_literal.rb +43 -0
- data/lib/mutineer/mutators/comparison.rb +36 -0
- data/lib/mutineer/mutators/condition_negation.rb +40 -0
- data/lib/mutineer/mutators/literal_mutation.rb +40 -0
- data/lib/mutineer/mutators/return_nil.rb +63 -0
- data/lib/mutineer/mutators/statement_removal.rb +34 -0
- data/lib/mutineer/parser.rb +26 -0
- data/lib/mutineer/project.rb +69 -0
- data/lib/mutineer/reporter.rb +204 -0
- data/lib/mutineer/result.rb +77 -0
- data/lib/mutineer/runner.rb +175 -0
- data/lib/mutineer/subject.rb +19 -0
- data/lib/mutineer/version.rb +5 -0
- data/lib/mutineer/worker_pool.rb +112 -0
- data/lib/mutineer.rb +29 -0
- metadata +104 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
require_relative "project"
|
|
5
|
+
require_relative "result"
|
|
6
|
+
require_relative "isolation"
|
|
7
|
+
require_relative "minitest_integration"
|
|
8
|
+
require_relative "coverage_map"
|
|
9
|
+
require_relative "mutator_registry"
|
|
10
|
+
require_relative "worker_pool"
|
|
11
|
+
|
|
12
|
+
module Mutineer
|
|
13
|
+
# Orchestrates one mutation end-to-end: apply it textually, validate the
|
|
14
|
+
# result, select its covering test files from the coverage map, then run only
|
|
15
|
+
# those against the mutated source in an isolated child process (strategy 7a —
|
|
16
|
+
# whole-file reload via `load`).
|
|
17
|
+
#
|
|
18
|
+
# The source file path is passed explicitly because Mutation carries only byte
|
|
19
|
+
# offsets, not its file. M3 replaces M2's hardcoded `test_file:` with coverage-
|
|
20
|
+
# map selection: a mutation whose line no test exercises is :no_coverage (no
|
|
21
|
+
# fork); otherwise exactly the covering test files run in the child.
|
|
22
|
+
class Runner
|
|
23
|
+
# Full Phase B orchestration: resolve operators, discover subjects, build the
|
|
24
|
+
# coverage map, run every mutation, and aggregate. Returns
|
|
25
|
+
# [AggregateResult, source_map]. The CLI then reports + applies the exit code;
|
|
26
|
+
# the integration test asserts directly on the AggregateResult.
|
|
27
|
+
#
|
|
28
|
+
# The parent process `require`s each source file so its classes exist; forked
|
|
29
|
+
# children inherit them, so a covering test file's own require_relative of the
|
|
30
|
+
# source is a no-op and does not clobber the mutated `load` (spec §7).
|
|
31
|
+
def self.execute(config)
|
|
32
|
+
operator_classes = MutatorRegistry.resolve(config.operators || MutatorRegistry::DEFAULT_NAMES)
|
|
33
|
+
|
|
34
|
+
# Boot mode: require the boot file ONCE so the app env (e.g. Rails) is booted
|
|
35
|
+
# in the parent and inherited by every fork. Do NOT manually require the
|
|
36
|
+
# sources — under Zeitwerk a manual require of an autoloadable file raises;
|
|
37
|
+
# the booted env autoloads them, and subject discovery is a static Prism
|
|
38
|
+
# parse that needs nothing loaded. Standalone mode requires the sources as
|
|
39
|
+
# before so their classes exist for the children to inherit.
|
|
40
|
+
if config.boot
|
|
41
|
+
require File.expand_path(config.boot, config.project_root)
|
|
42
|
+
else
|
|
43
|
+
config.sources.each { |f| require File.expand_path(f, config.project_root) }
|
|
44
|
+
end
|
|
45
|
+
config.require_paths.each { |f| require File.expand_path(f, config.project_root) }
|
|
46
|
+
|
|
47
|
+
# Boot mode skips coverage selection entirely: every mutant runs the given
|
|
48
|
+
# --test files (precomputed absolute here, used directly by Runner.run).
|
|
49
|
+
if config.boot
|
|
50
|
+
coverage_map = nil
|
|
51
|
+
boot_tests = config.tests.map { |t| File.expand_path(t, config.project_root) }
|
|
52
|
+
# Rails/Minitest test files do `require "test_helper"`, which needs the
|
|
53
|
+
# test root on $LOAD_PATH (`bin/rails test` adds it). Prepend each test
|
|
54
|
+
# file's helper root here in the parent so loading them in the fork
|
|
55
|
+
# children resolves. Inherited by every fork.
|
|
56
|
+
test_load_roots(boot_tests).each { |d| $LOAD_PATH.unshift(d) unless $LOAD_PATH.include?(d) }
|
|
57
|
+
else
|
|
58
|
+
coverage_map = CoverageMap.new(
|
|
59
|
+
source_paths: config.sources, test_paths: config.tests,
|
|
60
|
+
cache_dir: config.cache_dir, project_root: config.project_root,
|
|
61
|
+
load_paths: config.load_paths
|
|
62
|
+
).build_or_load
|
|
63
|
+
boot_tests = nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Collect every (subject, mutation) up front so the pool can fan them out.
|
|
67
|
+
source_map = {}
|
|
68
|
+
jobs = []
|
|
69
|
+
Project.discover(config.sources, only: config.only).each do |subject|
|
|
70
|
+
source = (source_map[subject.file] ||= File.read(subject.file))
|
|
71
|
+
operator_classes.each do |klass|
|
|
72
|
+
klass.new.mutations_for(subject, source).each do |mutation|
|
|
73
|
+
jobs << [subject, mutation]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# C3: 7a writes mutineer_mutant*.rb into each source dir (so require_relative
|
|
79
|
+
# resolves). A SIGKILL'd child skips the tempfile's ensure-unlink, orphaning
|
|
80
|
+
# it. `ensure` is unreliable vs SIGKILL, so the PARENT sweeps each source dir
|
|
81
|
+
# before and after the run — orphans are impossible after a normal run.
|
|
82
|
+
source_dirs = config.sources
|
|
83
|
+
.map { |f| File.dirname(File.expand_path(f, config.project_root)) }.uniq
|
|
84
|
+
sweep_orphans(source_dirs)
|
|
85
|
+
|
|
86
|
+
strategy = config.strategy
|
|
87
|
+
results =
|
|
88
|
+
begin
|
|
89
|
+
bare = WorkerPool.new(config.jobs).run(jobs) do |subject, mutation|
|
|
90
|
+
run(mutation, source_file: subject.file, coverage_map: coverage_map,
|
|
91
|
+
subject: subject, strategy: strategy,
|
|
92
|
+
test_files: boot_tests, rails: config.rails)
|
|
93
|
+
end
|
|
94
|
+
# The bare Results carry only status (Subjects hold live AST nodes that
|
|
95
|
+
# do not marshal); reattach subject+mutation in the parent, in order.
|
|
96
|
+
bare.each_with_index.map { |r, i| r.with(subject: jobs[i][0], mutation: jobs[i][1]) }
|
|
97
|
+
ensure
|
|
98
|
+
sweep_orphans(source_dirs)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
[AggregateResult.new(results), source_map]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# For each test file, the directory to add to $LOAD_PATH so its
|
|
105
|
+
# `require "test_helper"` (or spec_helper) resolves: the nearest ancestor
|
|
106
|
+
# holding that helper, plus the file's own dir as a fallback.
|
|
107
|
+
def self.test_load_roots(test_files)
|
|
108
|
+
test_files.flat_map do |f|
|
|
109
|
+
dir = File.dirname(f)
|
|
110
|
+
root = nil
|
|
111
|
+
loop do
|
|
112
|
+
if File.exist?(File.join(dir, "test_helper.rb")) || File.exist?(File.join(dir, "spec_helper.rb"))
|
|
113
|
+
root = dir
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
parent = File.dirname(dir)
|
|
117
|
+
break if parent == dir
|
|
118
|
+
|
|
119
|
+
dir = parent
|
|
120
|
+
end
|
|
121
|
+
[root, File.dirname(f)].compact
|
|
122
|
+
end.uniq
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.sweep_orphans(dirs)
|
|
126
|
+
dirs.each do |dir|
|
|
127
|
+
Dir.glob(File.join(dir, "mutineer_mutant*.rb")).each do |f|
|
|
128
|
+
File.unlink(f) rescue nil # rubocop:disable Style/RescueModifier
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.run(mutation, source_file:, coverage_map: nil, subject: nil, strategy: "reload",
|
|
134
|
+
timeout: Isolation::DEFAULT_TIMEOUT, test_files: nil, rails: false)
|
|
135
|
+
source = File.read(source_file)
|
|
136
|
+
mutated = mutation.apply(source)
|
|
137
|
+
|
|
138
|
+
# Validity rule: a mutant that doesn't re-parse is skipped before forking.
|
|
139
|
+
return Result.skipped if Parser.parse_string(mutated).errors.any?
|
|
140
|
+
|
|
141
|
+
# Boot mode passes its tests directly (already absolute); standalone mode
|
|
142
|
+
# selects covering tests from the map and is :no_coverage when none cover it.
|
|
143
|
+
if test_files
|
|
144
|
+
abs_tests = test_files
|
|
145
|
+
else
|
|
146
|
+
line = source.byteslice(0, mutation.start_offset).count("\n") + 1
|
|
147
|
+
chosen = coverage_map.tests_for(source_file, line)
|
|
148
|
+
return Result.no_coverage if chosen.empty?
|
|
149
|
+
|
|
150
|
+
abs_tests = chosen.map { |t| File.expand_path(t, coverage_map.project_root) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
Isolation.run(timeout: timeout) do
|
|
154
|
+
# Forking inherits the parent's live DB connection; sharing one socket
|
|
155
|
+
# across processes corrupts it. Drop it so AR reconnects per child.
|
|
156
|
+
reconnect_active_record if rails
|
|
157
|
+
if strategy == "redefine"
|
|
158
|
+
Isolation.apply_surgical(mutation, subject, source)
|
|
159
|
+
else
|
|
160
|
+
Isolation.apply_whole_file(mutated, source_file)
|
|
161
|
+
end
|
|
162
|
+
MinitestIntegration.run(abs_tests)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.reconnect_active_record
|
|
167
|
+
return unless defined?(ActiveRecord::Base)
|
|
168
|
+
|
|
169
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
private_class_method :reconnect_active_record
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutineer
|
|
4
|
+
# One discoverable method: its location, namespace context, and the live
|
|
5
|
+
# Prism::DefNode mutators walk. Struct (not Data) because def_node is a live
|
|
6
|
+
# AST node — value-equality would be hollow, so we don't promise it.
|
|
7
|
+
Subject = Struct.new(:file, :namespace, :name, :singleton, :def_node, keyword_init: true) do
|
|
8
|
+
# e.g. "Billing::Invoice#total", "Billing::Invoice.build".
|
|
9
|
+
# Top-level (empty namespace) -> "#name" (no :: prefix).
|
|
10
|
+
def qualified_name
|
|
11
|
+
namespace.join("::") + (singleton ? "." : "#") + name.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# nil for empty methods (def empty; end).
|
|
15
|
+
def body_loc
|
|
16
|
+
def_node.body&.location
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "result"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# Fixed-size fork pool (KTD1/KTD2). `run` forks up to `size` children at once;
|
|
7
|
+
# each child runs the block on one work item, marshals its Result to a private
|
|
8
|
+
# pipe, and exits. The parent reaps any finished child with Process.wait2(-1),
|
|
9
|
+
# opening exactly one slot per reap, then refills. Results are returned in the
|
|
10
|
+
# SAME ORDER as `items` regardless of finish order, so verdicts are identical to
|
|
11
|
+
# a serial run (R4) and downstream output is stable.
|
|
12
|
+
#
|
|
13
|
+
# The block is evaluated inside the child via `yield(*items[i])`; whatever it
|
|
14
|
+
# returns (a Result) is the marshaled payload. Per-mutant timeout is handled one
|
|
15
|
+
# level down by Isolation (KTD2) — the pool adds no separate wall clock.
|
|
16
|
+
class WorkerPool
|
|
17
|
+
def initialize(size)
|
|
18
|
+
@size = [size.to_i, 1].max
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(items)
|
|
22
|
+
results = Array.new(items.size)
|
|
23
|
+
queue = (0...items.size).to_a
|
|
24
|
+
running = {} # pid => [index, read_io]
|
|
25
|
+
|
|
26
|
+
until queue.empty? && running.empty?
|
|
27
|
+
fill(items, queue, running) { |*args| yield(*args) }
|
|
28
|
+
reap(results, running)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
results
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def fill(items, queue, running)
|
|
37
|
+
while running.size < @size && !queue.empty?
|
|
38
|
+
idx = queue.shift
|
|
39
|
+
rd, wr = IO.pipe
|
|
40
|
+
begin
|
|
41
|
+
pid = fork do
|
|
42
|
+
rd.close
|
|
43
|
+
# R1: the child must ALWAYS hard-exit. If yield raises, marshal an
|
|
44
|
+
# error Result and exit! in `ensure` — otherwise the child unwinds
|
|
45
|
+
# normally and our Minitest at_exit autorun re-runs the parent suite
|
|
46
|
+
# inside the worker, losing the real error.
|
|
47
|
+
payload =
|
|
48
|
+
begin
|
|
49
|
+
yield(*items[idx])
|
|
50
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
51
|
+
Result.error("worker crashed: #{e.class}: #{e.message}")
|
|
52
|
+
end
|
|
53
|
+
begin
|
|
54
|
+
wr.write(Marshal.dump(payload))
|
|
55
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
56
|
+
# pipe gone; parent will record "no result"
|
|
57
|
+
ensure
|
|
58
|
+
wr.close
|
|
59
|
+
exit!(0)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
rescue Errno::EAGAIN
|
|
63
|
+
# Process table is full. Put the item back and reap before retrying;
|
|
64
|
+
# if nothing is running we cannot make progress, so re-raise.
|
|
65
|
+
rd.close
|
|
66
|
+
wr.close
|
|
67
|
+
raise if running.empty?
|
|
68
|
+
|
|
69
|
+
queue.unshift(idx)
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
wr.close
|
|
73
|
+
running[pid] = [idx, rd]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# R6: reap exactly one of OUR children. wait2(-1) would reap (steal) any of
|
|
78
|
+
# the host process's children — fatal when the pool runs under a forking test
|
|
79
|
+
# suite. Poll our known pids with WNOHANG instead.
|
|
80
|
+
def reap(results, running)
|
|
81
|
+
return if running.empty?
|
|
82
|
+
|
|
83
|
+
loop do
|
|
84
|
+
running.each_key do |pid|
|
|
85
|
+
reaped, = Process.waitpid2(pid, Process::WNOHANG)
|
|
86
|
+
next unless reaped
|
|
87
|
+
|
|
88
|
+
return collect(results, running, pid)
|
|
89
|
+
end
|
|
90
|
+
sleep 0.005
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def collect(results, running, pid)
|
|
95
|
+
idx, rd = running.delete(pid)
|
|
96
|
+
data = rd.read
|
|
97
|
+
rd.close
|
|
98
|
+
results[idx] =
|
|
99
|
+
if data.empty?
|
|
100
|
+
Result.error("worker produced no result")
|
|
101
|
+
else
|
|
102
|
+
# R6: a partial/garbage Marshal stream (dead worker) must not crash the
|
|
103
|
+
# pool — degrade to an error Result.
|
|
104
|
+
begin
|
|
105
|
+
Marshal.load(data)
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Result.error("worker result unreadable: #{e.class}: #{e.message}")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/mutineer.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mutineer/version"
|
|
4
|
+
require_relative "mutineer/config"
|
|
5
|
+
require_relative "mutineer/parser"
|
|
6
|
+
require_relative "mutineer/subject"
|
|
7
|
+
require_relative "mutineer/mutation"
|
|
8
|
+
require_relative "mutineer/project"
|
|
9
|
+
require_relative "mutineer/result"
|
|
10
|
+
require_relative "mutineer/coverage_map"
|
|
11
|
+
require_relative "mutineer/isolation"
|
|
12
|
+
require_relative "mutineer/minitest_integration"
|
|
13
|
+
require_relative "mutineer/mutators/base"
|
|
14
|
+
require_relative "mutineer/mutators/arithmetic"
|
|
15
|
+
require_relative "mutineer/mutators/comparison"
|
|
16
|
+
require_relative "mutineer/mutators/boolean_connector"
|
|
17
|
+
require_relative "mutineer/mutators/boolean_literal"
|
|
18
|
+
require_relative "mutineer/mutators/statement_removal"
|
|
19
|
+
require_relative "mutineer/mutators/return_nil"
|
|
20
|
+
require_relative "mutineer/mutators/literal_mutation"
|
|
21
|
+
require_relative "mutineer/mutators/condition_negation"
|
|
22
|
+
require_relative "mutineer/mutator_registry"
|
|
23
|
+
require_relative "mutineer/worker_pool"
|
|
24
|
+
require_relative "mutineer/runner"
|
|
25
|
+
require_relative "mutineer/reporter"
|
|
26
|
+
require_relative "mutineer/cli"
|
|
27
|
+
|
|
28
|
+
module Mutineer
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mutineer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Teren
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
description: Mutineer mutates your source one change at a time and runs your Minitest
|
|
41
|
+
suite against each mutant to find tests that don't actually test anything. Prism-based,
|
|
42
|
+
fork-isolated, zero runtime dependencies.
|
|
43
|
+
email:
|
|
44
|
+
- dteren@gmail.com
|
|
45
|
+
executables:
|
|
46
|
+
- mutineer
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- bin/mutineer
|
|
54
|
+
- lib/mutineer.rb
|
|
55
|
+
- lib/mutineer/cli.rb
|
|
56
|
+
- lib/mutineer/config.rb
|
|
57
|
+
- lib/mutineer/coverage_map.rb
|
|
58
|
+
- lib/mutineer/isolation.rb
|
|
59
|
+
- lib/mutineer/minitest_integration.rb
|
|
60
|
+
- lib/mutineer/mutation.rb
|
|
61
|
+
- lib/mutineer/mutator_registry.rb
|
|
62
|
+
- lib/mutineer/mutators/arithmetic.rb
|
|
63
|
+
- lib/mutineer/mutators/base.rb
|
|
64
|
+
- lib/mutineer/mutators/boolean_connector.rb
|
|
65
|
+
- lib/mutineer/mutators/boolean_literal.rb
|
|
66
|
+
- lib/mutineer/mutators/comparison.rb
|
|
67
|
+
- lib/mutineer/mutators/condition_negation.rb
|
|
68
|
+
- lib/mutineer/mutators/literal_mutation.rb
|
|
69
|
+
- lib/mutineer/mutators/return_nil.rb
|
|
70
|
+
- lib/mutineer/mutators/statement_removal.rb
|
|
71
|
+
- lib/mutineer/parser.rb
|
|
72
|
+
- lib/mutineer/project.rb
|
|
73
|
+
- lib/mutineer/reporter.rb
|
|
74
|
+
- lib/mutineer/result.rb
|
|
75
|
+
- lib/mutineer/runner.rb
|
|
76
|
+
- lib/mutineer/subject.rb
|
|
77
|
+
- lib/mutineer/version.rb
|
|
78
|
+
- lib/mutineer/worker_pool.rb
|
|
79
|
+
homepage: https://github.com/davidteren/mutineer
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata:
|
|
83
|
+
source_code_uri: https://github.com/davidteren/mutineer
|
|
84
|
+
changelog_uri: https://github.com/davidteren/mutineer/blob/main/CHANGELOG.md
|
|
85
|
+
bug_tracker_uri: https://github.com/davidteren/mutineer/issues
|
|
86
|
+
rubygems_mfa_required: 'true'
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '3.4'
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.6.9
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: A clean-room mutation-testing tool for Ruby (Prism + stdlib only).
|
|
104
|
+
test_files: []
|