mutineer 0.7.0 → 0.9.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 +49 -0
- data/README.md +39 -4
- data/lib/mutineer/baseline.rb +29 -13
- data/lib/mutineer/changed_lines.rb +29 -12
- data/lib/mutineer/cli.rb +88 -20
- data/lib/mutineer/config.rb +24 -2
- data/lib/mutineer/coverage_map.rb +86 -4
- data/lib/mutineer/isolation.rb +89 -40
- data/lib/mutineer/minitest_integration.rb +13 -9
- data/lib/mutineer/mutant_id.rb +24 -5
- data/lib/mutineer/mutation.rb +12 -6
- data/lib/mutineer/mutator_registry.rb +33 -6
- 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/collection_method.rb +44 -0
- 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/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +22 -7
- data/lib/mutineer/mutators/statement_removal.rb +6 -9
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/pairing.rb +32 -13
- data/lib/mutineer/parser.rb +17 -7
- data/lib/mutineer/project.rb +73 -2
- data/lib/mutineer/reporter.rb +169 -0
- data/lib/mutineer/result.rb +100 -32
- data/lib/mutineer/runner.rb +29 -2
- 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 +51 -29
- data/lib/mutineer.rb +4 -0
- metadata +18 -1
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,29 +3,47 @@
|
|
|
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
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Runs the work items through the pool.
|
|
26
|
+
#
|
|
27
|
+
# @param items [Array<Array>] work items.
|
|
28
|
+
# @param stop_when [Proc, nil] called with each collected Result; when it
|
|
29
|
+
# returns truthy, no further items are scheduled and the run drains and
|
|
30
|
+
# returns early (#21 --fail-fast). Unscheduled slots stay nil.
|
|
31
|
+
# @yieldparam item [Array] one work item.
|
|
32
|
+
# @return [Array<Mutineer::Result>] results in input order (nil for any item
|
|
33
|
+
# left unscheduled by an early stop).
|
|
34
|
+
def run(items, stop_when: nil)
|
|
35
|
+
results = Array.new(items.size)
|
|
36
|
+
queue = (0...items.size).to_a
|
|
37
|
+
running = {} # pid => [index, read_io, buffer]
|
|
38
|
+
stopping = false
|
|
25
39
|
|
|
26
40
|
until queue.empty? && running.empty?
|
|
27
|
-
fill(items, queue, running) { |*args| yield(*args) }
|
|
28
|
-
reap(results, running)
|
|
41
|
+
fill(items, queue, running) { |*args| yield(*args) } unless stopping
|
|
42
|
+
result = reap(results, running)
|
|
43
|
+
if !stopping && stop_when && result && stop_when.call(result)
|
|
44
|
+
stopping = true
|
|
45
|
+
queue.clear # schedule no more; let in-flight workers drain
|
|
46
|
+
end
|
|
29
47
|
end
|
|
30
48
|
|
|
31
49
|
results
|
|
@@ -33,17 +51,19 @@ module Mutineer
|
|
|
33
51
|
|
|
34
52
|
private
|
|
35
53
|
|
|
54
|
+
# R1: the child must ALWAYS hard-exit. If yield raises, marshal an error
|
|
55
|
+
# Result and exit! in `ensure` — otherwise the child unwinds normally and
|
|
56
|
+
# our Minitest at_exit autorun re-runs the parent suite inside the worker,
|
|
57
|
+
# losing the real error.
|
|
36
58
|
def fill(items, queue, running)
|
|
37
59
|
while running.size < @size && !queue.empty?
|
|
38
60
|
idx = queue.shift
|
|
39
61
|
rd, wr = IO.pipe
|
|
62
|
+
rd.binmode # #19: Marshal output is binary — keep the pipe byte-exact
|
|
63
|
+
wr.binmode
|
|
40
64
|
begin
|
|
41
65
|
pid = fork do
|
|
42
66
|
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
67
|
payload =
|
|
48
68
|
begin
|
|
49
69
|
yield(*items[idx])
|
|
@@ -74,13 +94,13 @@ module Mutineer
|
|
|
74
94
|
end
|
|
75
95
|
end
|
|
76
96
|
|
|
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).
|
|
97
|
+
# Drain pipes with IO.select and reap a child only on EOF (#4). The old
|
|
98
|
+
# code reaped first and read after — but a child whose payload exceeds the
|
|
99
|
+
# OS pipe buffer (~64KB) blocks on `write` before it can exit, so it was
|
|
100
|
+
# never reaped and the pool deadlocked. Reading concurrently keeps the
|
|
101
|
+
# pipe drained so the child can finish and exit; EOF means it closed its
|
|
102
|
+
# write end (done writing). We waitpid only OUR known pids (R6: never
|
|
103
|
+
# wait2(-1), which would steal the host suite's children).
|
|
84
104
|
def reap(results, running)
|
|
85
105
|
return if running.empty?
|
|
86
106
|
|
|
@@ -96,19 +116,21 @@ module Mutineer
|
|
|
96
116
|
rd.close
|
|
97
117
|
Process.waitpid(pid) # reap the now-finished child (no zombie)
|
|
98
118
|
running.delete(pid)
|
|
99
|
-
|
|
100
|
-
return
|
|
119
|
+
# Return the collected Result so the caller's stop_when (#21) can see it.
|
|
120
|
+
return results[idx] = decode(buf)
|
|
101
121
|
end
|
|
102
122
|
buf << chunk
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
end
|
|
106
126
|
|
|
127
|
+
# R6: a partial/garbage Marshal stream (dead worker) must not crash the
|
|
128
|
+
# pool — degrade to an error Result.
|
|
129
|
+
# @param data [String] marshaled payload.
|
|
130
|
+
# @return [Mutineer::Result] decoded result or error result.
|
|
107
131
|
def decode(data)
|
|
108
132
|
return Result.error("worker produced no result") if data.empty?
|
|
109
133
|
|
|
110
|
-
# R6: a partial/garbage Marshal stream (dead worker) must not crash the
|
|
111
|
-
# pool — degrade to an error Result.
|
|
112
134
|
Marshal.load(data)
|
|
113
135
|
rescue StandardError => e
|
|
114
136
|
Result.error("worker result unreadable: #{e.class}: #{e.message}")
|
data/lib/mutineer.rb
CHANGED
|
@@ -22,6 +22,9 @@ require_relative "mutineer/mutators/statement_removal"
|
|
|
22
22
|
require_relative "mutineer/mutators/return_nil"
|
|
23
23
|
require_relative "mutineer/mutators/literal_mutation"
|
|
24
24
|
require_relative "mutineer/mutators/condition_negation"
|
|
25
|
+
require_relative "mutineer/mutators/string_literal"
|
|
26
|
+
require_relative "mutineer/mutators/regex_literal"
|
|
27
|
+
require_relative "mutineer/mutators/collection_method"
|
|
25
28
|
require_relative "mutineer/mutator_registry"
|
|
26
29
|
require_relative "mutineer/worker_pool"
|
|
27
30
|
require_relative "mutineer/runner"
|
|
@@ -29,5 +32,6 @@ require_relative "mutineer/reporter"
|
|
|
29
32
|
require_relative "mutineer/baseline"
|
|
30
33
|
require_relative "mutineer/cli"
|
|
31
34
|
|
|
35
|
+
# Mutineer is the top-level namespace for the mutation-testing library.
|
|
32
36
|
module Mutineer
|
|
33
37
|
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.9.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.
|
|
@@ -66,11 +80,14 @@ files:
|
|
|
66
80
|
- lib/mutineer/mutators/base.rb
|
|
67
81
|
- lib/mutineer/mutators/boolean_connector.rb
|
|
68
82
|
- lib/mutineer/mutators/boolean_literal.rb
|
|
83
|
+
- lib/mutineer/mutators/collection_method.rb
|
|
69
84
|
- lib/mutineer/mutators/comparison.rb
|
|
70
85
|
- lib/mutineer/mutators/condition_negation.rb
|
|
71
86
|
- lib/mutineer/mutators/literal_mutation.rb
|
|
87
|
+
- lib/mutineer/mutators/regex_literal.rb
|
|
72
88
|
- lib/mutineer/mutators/return_nil.rb
|
|
73
89
|
- lib/mutineer/mutators/statement_removal.rb
|
|
90
|
+
- lib/mutineer/mutators/string_literal.rb
|
|
74
91
|
- lib/mutineer/pairing.rb
|
|
75
92
|
- lib/mutineer/parser.rb
|
|
76
93
|
- lib/mutineer/project.rb
|