mutineer 0.6.2 → 0.8.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 +4 -4
- data/CHANGELOG.md +59 -0
- data/README.md +63 -2
- data/lib/mutineer/baseline.rb +86 -0
- data/lib/mutineer/changed_lines.rb +29 -12
- data/lib/mutineer/cli.rb +137 -9
- data/lib/mutineer/config.rb +30 -2
- data/lib/mutineer/coverage_map.rb +135 -11
- data/lib/mutineer/isolation.rb +89 -40
- data/lib/mutineer/minitest_integration.rb +13 -9
- data/lib/mutineer/mutant_id.rb +66 -0
- data/lib/mutineer/mutation.rb +12 -6
- data/lib/mutineer/mutator_registry.rb +21 -3
- data/lib/mutineer/mutators/arithmetic.rb +8 -2
- data/lib/mutineer/mutators/base.rb +11 -3
- data/lib/mutineer/mutators/boolean_connector.rb +15 -5
- data/lib/mutineer/mutators/boolean_literal.rb +19 -6
- data/lib/mutineer/mutators/comparison.rb +7 -8
- data/lib/mutineer/mutators/condition_negation.rb +14 -5
- data/lib/mutineer/mutators/literal_mutation.rb +15 -4
- data/lib/mutineer/mutators/return_nil.rb +22 -7
- data/lib/mutineer/mutators/statement_removal.rb +6 -9
- data/lib/mutineer/pairing.rb +86 -0
- data/lib/mutineer/parser.rb +17 -7
- data/lib/mutineer/project.rb +73 -2
- data/lib/mutineer/reporter.rb +172 -8
- data/lib/mutineer/result.rb +128 -32
- data/lib/mutineer/runner.rb +101 -9
- data/lib/mutineer/subject.rb +10 -6
- data/lib/mutineer/test_runners/minitest.rb +5 -4
- data/lib/mutineer/test_runners/rspec.rb +10 -17
- data/lib/mutineer/test_runners.rb +9 -2
- data/lib/mutineer/version.rb +2 -1
- data/lib/mutineer/worker_pool.rb +34 -21
- data/lib/mutineer.rb +3 -0
- metadata +18 -1
data/lib/mutineer/runner.rb
CHANGED
|
@@ -10,6 +10,8 @@ require_relative "coverage_map"
|
|
|
10
10
|
require_relative "changed_lines"
|
|
11
11
|
require_relative "mutator_registry"
|
|
12
12
|
require_relative "worker_pool"
|
|
13
|
+
require_relative "mutant_id"
|
|
14
|
+
require "set"
|
|
13
15
|
|
|
14
16
|
module Mutineer
|
|
15
17
|
# Orchestrates one mutation end-to-end: apply it textually, validate the
|
|
@@ -30,6 +32,9 @@ module Mutineer
|
|
|
30
32
|
# The parent process `require`s each source file so its classes exist; forked
|
|
31
33
|
# children inherit them, so a covering test file's own require_relative of the
|
|
32
34
|
# source is a no-op and does not clobber the mutated `load` (spec §7).
|
|
35
|
+
#
|
|
36
|
+
# @param config [Mutineer::Config] run configuration.
|
|
37
|
+
# @return [Array(Mutineer::AggregateResult, Hash<String, String>)] aggregate and source map.
|
|
33
38
|
def self.execute(config)
|
|
34
39
|
operator_classes = MutatorRegistry.resolve(config.operators || MutatorRegistry::DEFAULT_NAMES)
|
|
35
40
|
|
|
@@ -70,7 +75,8 @@ module Mutineer
|
|
|
70
75
|
source_paths: config.sources, test_paths: config.tests,
|
|
71
76
|
cache_dir: config.cache_dir, project_root: config.project_root,
|
|
72
77
|
load_paths: config.load_paths, framework: config.framework,
|
|
73
|
-
boot_path: File.expand_path(config.boot, config.project_root)
|
|
78
|
+
boot_path: File.expand_path(config.boot, config.project_root),
|
|
79
|
+
verbose: config.verbose
|
|
74
80
|
).build_via_fork(rails: config.rails)
|
|
75
81
|
else
|
|
76
82
|
coverage_map = CoverageMap.new(
|
|
@@ -81,13 +87,28 @@ module Mutineer
|
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
# Collect every (subject, mutation) up front so the pool can fan them out.
|
|
90
|
+
# #10: a mutant the user marked known-equivalent (inline disable-line comment
|
|
91
|
+
# or .mutineer.yml ignore id) is classified :ignored here and NEVER forked —
|
|
92
|
+
# it is removed from the killed+survived denominator so a strong file reaches
|
|
93
|
+
# 100%. The stable id is computed per subject (occurrence needs the full list)
|
|
94
|
+
# and carried on every job so the parent can reattach it after the run.
|
|
84
95
|
source_map = {}
|
|
96
|
+
disabled_map = {}
|
|
97
|
+
ignore_set = config.ignore.to_set
|
|
85
98
|
jobs = []
|
|
99
|
+
ignored_results = []
|
|
86
100
|
Project.discover(config.sources, only: config.only).each do |subject|
|
|
87
101
|
source = (source_map[subject.file] ||= File.read(subject.file))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
disabled = (disabled_map[subject.file] ||= suppress_map(source))
|
|
103
|
+
mutations = operator_classes.flat_map { |klass| klass.new.mutations_for(subject, source) }
|
|
104
|
+
ids = MutantId.for_subject(subject, source, mutations)
|
|
105
|
+
mutations.each_with_index do |mutation, i|
|
|
106
|
+
id = ids[i]
|
|
107
|
+
line = source.byteslice(0, mutation.start_offset).count("\n") + 1
|
|
108
|
+
if suppressed?(mutation.operator, line, id, disabled, ignore_set)
|
|
109
|
+
ignored_results << Result.ignored.with(subject: subject, mutation: mutation, id: id)
|
|
110
|
+
else
|
|
111
|
+
jobs << [subject, mutation, id]
|
|
91
112
|
end
|
|
92
113
|
end
|
|
93
114
|
end
|
|
@@ -111,13 +132,42 @@ module Mutineer
|
|
|
111
132
|
subject: subject, strategy: strategy, rails: config.rails, framework: framework)
|
|
112
133
|
end
|
|
113
134
|
# The bare Results carry only status (Subjects hold live AST nodes that
|
|
114
|
-
# do not marshal); reattach subject+mutation in the parent, in order.
|
|
115
|
-
bare.each_with_index.map { |r, i| r.with(subject: jobs[i][0], mutation: jobs[i][1]) }
|
|
135
|
+
# do not marshal); reattach subject+mutation+id in the parent, in order.
|
|
136
|
+
bare.each_with_index.map { |r, i| r.with(subject: jobs[i][0], mutation: jobs[i][1], id: jobs[i][2]) }
|
|
116
137
|
ensure
|
|
117
138
|
sweep_orphans(source_dirs)
|
|
118
139
|
end
|
|
119
140
|
|
|
120
|
-
[AggregateResult.new(results), source_map]
|
|
141
|
+
[AggregateResult.new(results + ignored_results), source_map]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Scan a source once into { line_number => :all | Set[operator_syms] } from
|
|
145
|
+
# inline `# mutineer:disable-line [ops]` markers (RuboCop semantics: the marker
|
|
146
|
+
# sits on the same physical line as the code it silences). A bare marker
|
|
147
|
+
# disables every operator on that line; `disable-line a, b` only the listed
|
|
148
|
+
# operators. Block-form disable/enable ranges are intentionally not supported.
|
|
149
|
+
def self.suppress_map(source)
|
|
150
|
+
map = {}
|
|
151
|
+
source.each_line.with_index(1) do |text, line|
|
|
152
|
+
next unless (m = text.match(/#\s*mutineer:disable-line(?:\s+([\w,\s]+))?/))
|
|
153
|
+
|
|
154
|
+
ops = m[1]
|
|
155
|
+
map[line] = ops ? ops.split(",").map { |o| o.strip.to_sym }.reject(&:empty?).to_set : :all
|
|
156
|
+
end
|
|
157
|
+
map
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# True when this mutant is suppressed: its line bears a disable-line marker
|
|
161
|
+
# (bare, or scoped to its operator), OR its stable id is in the config ignore
|
|
162
|
+
# list. Checked at job-build time so a suppressed mutant is never forked.
|
|
163
|
+
def self.suppressed?(operator, line, id, disabled, ignore_set)
|
|
164
|
+
return true if ignore_set.include?(id)
|
|
165
|
+
|
|
166
|
+
case (entry = disabled[line])
|
|
167
|
+
when :all then true
|
|
168
|
+
when Set then entry.include?(operator)
|
|
169
|
+
else false
|
|
170
|
+
end
|
|
121
171
|
end
|
|
122
172
|
|
|
123
173
|
# --since: keep only jobs whose mutation lands on a line changed since the git
|
|
@@ -168,6 +218,11 @@ module Mutineer
|
|
|
168
218
|
warn "[mutineer] RAILS_ENV was unset; defaulting to 'test' for --rails."
|
|
169
219
|
end
|
|
170
220
|
|
|
221
|
+
# Removes stale mutant tempfiles from the given directories.
|
|
222
|
+
#
|
|
223
|
+
# @api private
|
|
224
|
+
# @param dirs [Array<String>] directories to sweep.
|
|
225
|
+
# @return [void]
|
|
171
226
|
def self.sweep_orphans(dirs)
|
|
172
227
|
dirs.each do |dir|
|
|
173
228
|
Dir.glob(File.join(dir, "mutineer_mutant*.rb")).each do |f|
|
|
@@ -176,6 +231,17 @@ module Mutineer
|
|
|
176
231
|
end
|
|
177
232
|
end
|
|
178
233
|
|
|
234
|
+
# Runs a single mutation through isolation.
|
|
235
|
+
#
|
|
236
|
+
# @param mutation [Mutineer::Mutation] mutation to run.
|
|
237
|
+
# @param source_file [String] source file path.
|
|
238
|
+
# @param coverage_map [Mutineer::CoverageMap, nil] coverage map.
|
|
239
|
+
# @param subject [Mutineer::Subject, nil] subject for surgical strategy.
|
|
240
|
+
# @param strategy [String] mutation strategy.
|
|
241
|
+
# @param timeout [Integer] child timeout in seconds.
|
|
242
|
+
# @param rails [Boolean] whether Rails reconnect handling is enabled.
|
|
243
|
+
# @param framework [String] test framework name.
|
|
244
|
+
# @return [Mutineer::Result] mutant result.
|
|
179
245
|
def self.run(mutation, source_file:, coverage_map: nil, subject: nil, strategy: "reload",
|
|
180
246
|
timeout: Isolation::DEFAULT_TIMEOUT, rails: false, framework: "minitest")
|
|
181
247
|
source = File.read(source_file)
|
|
@@ -189,7 +255,11 @@ module Mutineer
|
|
|
189
255
|
# covering test files run in the child.
|
|
190
256
|
line = source.byteslice(0, mutation.start_offset).count("\n") + 1
|
|
191
257
|
chosen = coverage_map.tests_for(source_file, line)
|
|
192
|
-
|
|
258
|
+
# #9: distinguish a genuine coverage gap from a line whose would-be test
|
|
259
|
+
# errored during capture (coverage lost) — the latter is :uncapturable.
|
|
260
|
+
if chosen.empty?
|
|
261
|
+
return coverage_map.uncapturable_source?(source_file) ? Result.uncapturable : Result.no_coverage
|
|
262
|
+
end
|
|
193
263
|
|
|
194
264
|
abs_tests = chosen.map { |t| File.expand_path(t, coverage_map.project_root) }
|
|
195
265
|
|
|
@@ -206,13 +276,35 @@ module Mutineer
|
|
|
206
276
|
end
|
|
207
277
|
end
|
|
208
278
|
|
|
279
|
+
# Reconnects ActiveRecord in a forked child when available.
|
|
280
|
+
#
|
|
281
|
+
# @api private
|
|
282
|
+
# @return [void]
|
|
209
283
|
def self.reconnect_active_record
|
|
210
284
|
return unless defined?(ActiveRecord::Base)
|
|
211
285
|
|
|
212
|
-
ActiveRecord::Base
|
|
286
|
+
base = ActiveRecord::Base
|
|
287
|
+
# #8: clearing connections here drops an open transactional-fixture
|
|
288
|
+
# transaction, so the test loses its fixture rows and fails. Skip the clear
|
|
289
|
+
# when a transaction is open; otherwise clear (v0.2 per-fork write-safety).
|
|
290
|
+
return if fixture_transaction_open?(base)
|
|
291
|
+
|
|
292
|
+
base.connection_handler.clear_all_connections!
|
|
213
293
|
rescue StandardError
|
|
214
294
|
nil
|
|
215
295
|
end
|
|
216
296
|
private_class_method :reconnect_active_record
|
|
297
|
+
|
|
298
|
+
# Pure, injectable predicate: true when a transactional-fixture transaction is
|
|
299
|
+
# already open on the connection. Keys off open_transactions (KTD-2) so it is
|
|
300
|
+
# correct whenever the transaction exists, regardless of when it opened. Any
|
|
301
|
+
# probe error degrades safe to false -> caller clears (existing behaviour).
|
|
302
|
+
def self.fixture_transaction_open?(base)
|
|
303
|
+
pool = base.connection_pool
|
|
304
|
+
pool.active_connection? && base.connection.open_transactions.positive?
|
|
305
|
+
rescue StandardError
|
|
306
|
+
false
|
|
307
|
+
end
|
|
308
|
+
private_class_method :fixture_transaction_open?
|
|
217
309
|
end
|
|
218
310
|
end
|
data/lib/mutineer/subject.rb
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mutineer
|
|
4
|
-
# One discoverable method
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# One discoverable method and its AST node.
|
|
5
|
+
#
|
|
6
|
+
# Location, namespace context, and the live Prism::DefNode are kept together
|
|
7
|
+
# because mutators walk the def node directly.
|
|
7
8
|
Subject = Struct.new(:file, :namespace, :name, :singleton, :def_node, keyword_init: true) do
|
|
8
|
-
#
|
|
9
|
-
#
|
|
9
|
+
# Returns the fully-qualified subject name.
|
|
10
|
+
#
|
|
11
|
+
# @return [String] namespaced method name like `Billing::Invoice#total`.
|
|
10
12
|
def qualified_name
|
|
11
13
|
namespace.join("::") + (singleton ? "." : "#") + name.to_s
|
|
12
14
|
end
|
|
13
15
|
|
|
14
|
-
#
|
|
16
|
+
# Returns the body location for the subject, if any.
|
|
17
|
+
#
|
|
18
|
+
# @return [Prism::Location, nil] body location or nil for empty methods.
|
|
15
19
|
def body_loc
|
|
16
20
|
def_node.body&.location
|
|
17
21
|
end
|
|
@@ -4,11 +4,12 @@ require_relative "../minitest_integration"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module TestRunners
|
|
7
|
-
#
|
|
8
|
-
# selection (TestRunners.for) has one method shape across frameworks.
|
|
9
|
-
# MinitestIntegration stays the implementation — all its behaviour (autorun
|
|
10
|
-
# neutralisation, Runnable.reset, load, Minitest.run -> 0/1) is preserved.
|
|
7
|
+
# Thin wrapper around the shared Minitest integration runner.
|
|
11
8
|
module Minitest
|
|
9
|
+
# Runs the given Minitest files.
|
|
10
|
+
#
|
|
11
|
+
# @param test_files [String, Array<String>] one file or many files.
|
|
12
|
+
# @return [Integer] 0 on success, 1 on failure.
|
|
12
13
|
def self.run(test_files) = MinitestIntegration.run(test_files)
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -8,31 +8,21 @@ module Mutineer
|
|
|
8
8
|
class FrameworkUnavailable < StandardError; end
|
|
9
9
|
|
|
10
10
|
module TestRunners
|
|
11
|
-
# Child-process-only RSpec runner
|
|
12
|
-
# run the given spec files and return 0 (all passed) / 1 (any failure).
|
|
11
|
+
# Child-process-only RSpec runner.
|
|
13
12
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# No `rescue` around the run itself: Isolation.run's fork block is the single
|
|
18
|
-
# exception boundary (any exception there -> exit 2), keeping the 0/1 contract.
|
|
13
|
+
# Mirrors MinitestIntegration's contract: run the given spec files and
|
|
14
|
+
# return 0 (all passed) or 1 (any failure).
|
|
19
15
|
module RSpec
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# Runs the given RSpec files.
|
|
17
|
+
#
|
|
18
|
+
# @param spec_files [String, Array<String>] one file or many files.
|
|
19
|
+
# @return [Integer] 0 on success, 1 on failure.
|
|
22
20
|
def self.run(spec_files)
|
|
23
21
|
require_rspec!
|
|
24
22
|
|
|
25
|
-
# rspec/autorun (if a spec_helper required it) installs an at_exit run;
|
|
26
|
-
# neutralise it like Minitest.autorun so it never double-fires.
|
|
27
23
|
::RSpec::Core::Runner.disable_autorun!
|
|
28
|
-
|
|
29
|
-
# Drop any RSpec world/configuration inherited across runs in one process
|
|
30
|
-
# (e.g. successive forks of a booted parent) so examples never accumulate.
|
|
31
24
|
::RSpec.reset
|
|
32
25
|
|
|
33
|
-
# Silence RSpec's formatter output (and any stray puts/deprecations) so it
|
|
34
|
-
# never pollutes Mutineer's report streams. The formatter writes to the
|
|
35
|
-
# passed IO; $stdout/$stderr are redirected to catch everything else.
|
|
36
26
|
sink = StringIO.new
|
|
37
27
|
orig_out = $stdout
|
|
38
28
|
orig_err = $stderr
|
|
@@ -48,6 +38,9 @@ module Mutineer
|
|
|
48
38
|
status.zero? ? 0 : 1
|
|
49
39
|
end
|
|
50
40
|
|
|
41
|
+
# Requires rspec-core from the project under test.
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
51
44
|
def self.require_rspec!
|
|
52
45
|
require "rspec/core"
|
|
53
46
|
rescue LoadError
|
|
@@ -4,9 +4,16 @@ require_relative "test_runners/minitest"
|
|
|
4
4
|
require_relative "test_runners/rspec"
|
|
5
5
|
|
|
6
6
|
module Mutineer
|
|
7
|
-
# Picks the test-framework runner.
|
|
8
|
-
#
|
|
7
|
+
# Picks the test-framework runner.
|
|
8
|
+
#
|
|
9
|
+
# Each runner responds to `.run(files) -> 0/1` (0 = all passed, 1 = any
|
|
10
|
+
# failure) and is called only inside a forked child.
|
|
9
11
|
module TestRunners
|
|
12
|
+
# Returns the runner module for a framework name.
|
|
13
|
+
#
|
|
14
|
+
# @param framework [String, nil] framework name or nil.
|
|
15
|
+
# @return [Module] `Mutineer::TestRunners::Minitest` or `::RSpec`.
|
|
16
|
+
# @raise [Mutineer::ConfigError] when the framework is unknown.
|
|
10
17
|
def self.for(framework)
|
|
11
18
|
case framework
|
|
12
19
|
when "rspec" then RSpec
|
data/lib/mutineer/version.rb
CHANGED
data/lib/mutineer/worker_pool.rb
CHANGED
|
@@ -3,21 +3,30 @@
|
|
|
3
3
|
require_relative "result"
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
|
-
# Fixed-size fork pool (KTD1/KTD2). `run` forks up to `size` children at
|
|
7
|
-
# each child runs the block on one work item, marshals its Result to a
|
|
8
|
-
# pipe, and exits. The parent reaps any finished child with
|
|
9
|
-
# opening exactly one slot per reap, then refills.
|
|
10
|
-
# SAME ORDER as `items` regardless of finish
|
|
11
|
-
# a serial run (R4) and downstream output
|
|
6
|
+
# Fixed-size fork pool (KTD1/KTD2). `run` forks up to `size` children at
|
|
7
|
+
# once; each child runs the block on one work item, marshals its Result to a
|
|
8
|
+
# private pipe, and exits. The parent reaps any finished child with
|
|
9
|
+
# Process.wait2(-1), opening exactly one slot per reap, then refills.
|
|
10
|
+
# Results are returned in the SAME ORDER as `items` regardless of finish
|
|
11
|
+
# order, so verdicts are identical to a serial run (R4) and downstream output
|
|
12
|
+
# is stable.
|
|
12
13
|
#
|
|
13
14
|
# The block is run inside the child via `yield(*items[i])`; whatever it
|
|
14
|
-
# returns (a Result) is the marshaled payload. Per-mutant timeout is handled
|
|
15
|
-
# level down by Isolation (KTD2) — the pool adds no separate wall clock.
|
|
15
|
+
# returns (a Result) is the marshaled payload. Per-mutant timeout is handled
|
|
16
|
+
# one level down by Isolation (KTD2) — the pool adds no separate wall clock.
|
|
16
17
|
class WorkerPool
|
|
18
|
+
# Builds a pool.
|
|
19
|
+
#
|
|
20
|
+
# @param size [Integer] desired pool size.
|
|
17
21
|
def initialize(size)
|
|
18
22
|
@size = [size.to_i, 1].max
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
# Runs the work items through the pool.
|
|
26
|
+
#
|
|
27
|
+
# @param items [Array<Array>] work items.
|
|
28
|
+
# @yieldparam item [Array] one work item.
|
|
29
|
+
# @return [Array<Mutineer::Result>] results in input order.
|
|
21
30
|
def run(items)
|
|
22
31
|
results = Array.new(items.size)
|
|
23
32
|
queue = (0...items.size).to_a
|
|
@@ -33,17 +42,19 @@ module Mutineer
|
|
|
33
42
|
|
|
34
43
|
private
|
|
35
44
|
|
|
45
|
+
# R1: the child must ALWAYS hard-exit. If yield raises, marshal an error
|
|
46
|
+
# Result and exit! in `ensure` — otherwise the child unwinds normally and
|
|
47
|
+
# our Minitest at_exit autorun re-runs the parent suite inside the worker,
|
|
48
|
+
# losing the real error.
|
|
36
49
|
def fill(items, queue, running)
|
|
37
50
|
while running.size < @size && !queue.empty?
|
|
38
51
|
idx = queue.shift
|
|
39
52
|
rd, wr = IO.pipe
|
|
53
|
+
rd.binmode # #19: Marshal output is binary — keep the pipe byte-exact
|
|
54
|
+
wr.binmode
|
|
40
55
|
begin
|
|
41
56
|
pid = fork do
|
|
42
57
|
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
58
|
payload =
|
|
48
59
|
begin
|
|
49
60
|
yield(*items[idx])
|
|
@@ -74,13 +85,13 @@ module Mutineer
|
|
|
74
85
|
end
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
# Drain pipes with IO.select and reap a child only on EOF (#4). The old
|
|
78
|
-
# reaped first and read after — but a child whose payload exceeds the
|
|
79
|
-
# buffer (~64KB) blocks on `write` before it can exit, so it was
|
|
80
|
-
# and the pool deadlocked. Reading concurrently keeps the
|
|
81
|
-
# child can finish and exit; EOF means it closed its
|
|
82
|
-
# We waitpid only OUR known pids (R6: never
|
|
83
|
-
# host suite's children).
|
|
88
|
+
# Drain pipes with IO.select and reap a child only on EOF (#4). The old
|
|
89
|
+
# code reaped first and read after — but a child whose payload exceeds the
|
|
90
|
+
# OS pipe buffer (~64KB) blocks on `write` before it can exit, so it was
|
|
91
|
+
# never reaped and the pool deadlocked. Reading concurrently keeps the
|
|
92
|
+
# pipe drained so the child can finish and exit; EOF means it closed its
|
|
93
|
+
# write end (done writing). We waitpid only OUR known pids (R6: never
|
|
94
|
+
# wait2(-1), which would steal the host suite's children).
|
|
84
95
|
def reap(results, running)
|
|
85
96
|
return if running.empty?
|
|
86
97
|
|
|
@@ -104,11 +115,13 @@ module Mutineer
|
|
|
104
115
|
end
|
|
105
116
|
end
|
|
106
117
|
|
|
118
|
+
# R6: a partial/garbage Marshal stream (dead worker) must not crash the
|
|
119
|
+
# pool — degrade to an error Result.
|
|
120
|
+
# @param data [String] marshaled payload.
|
|
121
|
+
# @return [Mutineer::Result] decoded result or error result.
|
|
107
122
|
def decode(data)
|
|
108
123
|
return Result.error("worker produced no result") if data.empty?
|
|
109
124
|
|
|
110
|
-
# R6: a partial/garbage Marshal stream (dead worker) must not crash the
|
|
111
|
-
# pool — degrade to an error Result.
|
|
112
125
|
Marshal.load(data)
|
|
113
126
|
rescue StandardError => e
|
|
114
127
|
Result.error("worker result unreadable: #{e.class}: #{e.message}")
|
data/lib/mutineer.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "mutineer/parser"
|
|
|
6
6
|
require_relative "mutineer/subject"
|
|
7
7
|
require_relative "mutineer/mutation"
|
|
8
8
|
require_relative "mutineer/project"
|
|
9
|
+
require_relative "mutineer/pairing"
|
|
9
10
|
require_relative "mutineer/result"
|
|
10
11
|
require_relative "mutineer/coverage_map"
|
|
11
12
|
require_relative "mutineer/changed_lines"
|
|
@@ -25,7 +26,9 @@ require_relative "mutineer/mutator_registry"
|
|
|
25
26
|
require_relative "mutineer/worker_pool"
|
|
26
27
|
require_relative "mutineer/runner"
|
|
27
28
|
require_relative "mutineer/reporter"
|
|
29
|
+
require_relative "mutineer/baseline"
|
|
28
30
|
require_relative "mutineer/cli"
|
|
29
31
|
|
|
32
|
+
# Mutineer is the top-level namespace for the mutation-testing library.
|
|
30
33
|
module Mutineer
|
|
31
34
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mutineer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Teren
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: yard
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
40
54
|
description: Mutineer mutates your source one change at a time and runs your Minitest
|
|
41
55
|
suite against each mutant to find tests that don't actually test anything. Prism-based,
|
|
42
56
|
fork-isolated, zero runtime dependencies.
|
|
@@ -52,12 +66,14 @@ files:
|
|
|
52
66
|
- README.md
|
|
53
67
|
- bin/mutineer
|
|
54
68
|
- lib/mutineer.rb
|
|
69
|
+
- lib/mutineer/baseline.rb
|
|
55
70
|
- lib/mutineer/changed_lines.rb
|
|
56
71
|
- lib/mutineer/cli.rb
|
|
57
72
|
- lib/mutineer/config.rb
|
|
58
73
|
- lib/mutineer/coverage_map.rb
|
|
59
74
|
- lib/mutineer/isolation.rb
|
|
60
75
|
- lib/mutineer/minitest_integration.rb
|
|
76
|
+
- lib/mutineer/mutant_id.rb
|
|
61
77
|
- lib/mutineer/mutation.rb
|
|
62
78
|
- lib/mutineer/mutator_registry.rb
|
|
63
79
|
- lib/mutineer/mutators/arithmetic.rb
|
|
@@ -69,6 +85,7 @@ files:
|
|
|
69
85
|
- lib/mutineer/mutators/literal_mutation.rb
|
|
70
86
|
- lib/mutineer/mutators/return_nil.rb
|
|
71
87
|
- lib/mutineer/mutators/statement_removal.rb
|
|
88
|
+
- lib/mutineer/pairing.rb
|
|
72
89
|
- lib/mutineer/parser.rb
|
|
73
90
|
- lib/mutineer/project.rb
|
|
74
91
|
- lib/mutineer/reporter.rb
|