mutineer 0.7.0 → 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 +29 -0
- data/README.md +34 -1
- data/lib/mutineer/baseline.rb +29 -13
- data/lib/mutineer/changed_lines.rb +29 -12
- data/lib/mutineer/cli.rb +57 -0
- data/lib/mutineer/config.rb +21 -0
- 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 +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 +32 -13
- data/lib/mutineer/parser.rb +17 -7
- data/lib/mutineer/project.rb +73 -2
- data/lib/mutineer/reporter.rb +55 -0
- data/lib/mutineer/result.rb +100 -32
- data/lib/mutineer/runner.rb +23 -0
- 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 +1 -0
- metadata +15 -1
|
@@ -4,15 +4,15 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Return-
|
|
8
|
-
# 1. an explicit `return <expr>` -> `return nil`, unless the value is
|
|
9
|
-
# already nil (no-op guard).
|
|
10
|
-
# 2. a method body whose final expression is neither a ReturnNode nor a
|
|
11
|
-
# NilNode -> that expression becomes `nil`.
|
|
12
|
-
# Nested defs are their own subjects, so we never descend into them (R10).
|
|
7
|
+
# Return-nil mutator.
|
|
13
8
|
#
|
|
14
|
-
#
|
|
9
|
+
# Replaces explicit return values and final expressions with nil.
|
|
15
10
|
class ReturnNil < Base
|
|
11
|
+
# Collects return-nil mutations for a subject.
|
|
12
|
+
#
|
|
13
|
+
# @param subject [Mutineer::Subject] subject to inspect.
|
|
14
|
+
# @param source [String] full source text.
|
|
15
|
+
# @return [Array<Mutineer::Mutation>] collected mutations.
|
|
16
16
|
def mutations_for(subject, source)
|
|
17
17
|
@source = source
|
|
18
18
|
@mutations = []
|
|
@@ -24,6 +24,10 @@ module Mutineer
|
|
|
24
24
|
@mutations
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Visits return nodes.
|
|
28
|
+
#
|
|
29
|
+
# @param node [Prism::ReturnNode] node to inspect.
|
|
30
|
+
# @return [void]
|
|
27
31
|
def visit_return_node(node)
|
|
28
32
|
args = node.arguments
|
|
29
33
|
if args
|
|
@@ -37,10 +41,17 @@ module Mutineer
|
|
|
37
41
|
|
|
38
42
|
# Nested method definitions are discovered as their own subjects; do not
|
|
39
43
|
# recurse into them (prevents double-counting their statements).
|
|
44
|
+
#
|
|
45
|
+
# @param node [Prism::DefNode] nested definition node.
|
|
46
|
+
# @return [void]
|
|
40
47
|
def visit_def_node(node); end
|
|
41
48
|
|
|
42
49
|
private
|
|
43
50
|
|
|
51
|
+
# Mutates a method's final expression to nil when eligible.
|
|
52
|
+
#
|
|
53
|
+
# @param body [Prism::Node] method body node.
|
|
54
|
+
# @return [void]
|
|
44
55
|
def final_expression_nil(body)
|
|
45
56
|
return unless body.is_a?(Prism::StatementsNode)
|
|
46
57
|
|
|
@@ -50,6 +61,10 @@ module Mutineer
|
|
|
50
61
|
emit(last.location)
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# Emits a return-nil mutation.
|
|
65
|
+
#
|
|
66
|
+
# @param loc [Prism::Location] source location.
|
|
67
|
+
# @return [void]
|
|
53
68
|
def emit(loc)
|
|
54
69
|
@mutations << Mutation.new(
|
|
55
70
|
start_offset: loc.start_offset,
|
|
@@ -4,14 +4,14 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
6
|
module Mutators
|
|
7
|
-
# Statement-removal
|
|
8
|
-
# "nil". Tests whether the suite detects a missing side effect. The final
|
|
9
|
-
# expression is always skipped — replacing the return value with nil is the
|
|
10
|
-
# M5 return-nil operator's distinct concern (KTD-1). A body with < 2
|
|
11
|
-
# statements has no non-final statement, so it generates nothing.
|
|
7
|
+
# Statement-removal mutator.
|
|
12
8
|
#
|
|
13
|
-
#
|
|
9
|
+
# Replaces each non-final method statement with `nil`.
|
|
14
10
|
class StatementRemoval < Base
|
|
11
|
+
# Visits statement nodes and emits removals.
|
|
12
|
+
#
|
|
13
|
+
# @param node [Prism::StatementsNode] statement list to inspect.
|
|
14
|
+
# @return [void]
|
|
15
15
|
def visit_statements_node(node)
|
|
16
16
|
stmts = node.body
|
|
17
17
|
return if stmts.length < 2
|
|
@@ -25,9 +25,6 @@ module Mutineer
|
|
|
25
25
|
operator: :statement_removal
|
|
26
26
|
)
|
|
27
27
|
end
|
|
28
|
-
# ponytail: no super — recursing into a nested StatementsNode would
|
|
29
|
-
# re-emit removals already covered at the top level and double-count.
|
|
30
|
-
# Each subject's body is visited once.
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
end
|
data/lib/mutineer/pairing.rb
CHANGED
|
@@ -4,18 +4,24 @@ module Mutineer
|
|
|
4
4
|
# Source -> test pairing by path convention (#11). Pure stdlib path logic:
|
|
5
5
|
# no Rails, no class loading, no process. Two jobs:
|
|
6
6
|
# * expand_sources — a directory argument becomes its sorted **/*.rb files.
|
|
7
|
-
# * infer_test — a source's test file by convention (app/ and lib/
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# * infer_test — a source's test file by convention (app/ and lib/
|
|
8
|
+
# sources map to test/.../_test.rb or spec/.../_spec.rb),
|
|
9
|
+
# preserving namespaced subdirectories. First EXISTING
|
|
10
|
+
# candidate wins.
|
|
10
11
|
#
|
|
11
|
-
# Independently unit-testable: every method is pure in/out over the
|
|
12
|
-
# so the pairing contract is exercised with plain fixtures, no
|
|
12
|
+
# Independently unit-testable: every method is pure in/out over the
|
|
13
|
+
# filesystem, so the pairing contract is exercised with plain fixtures, no
|
|
14
|
+
# Rails, no fork.
|
|
13
15
|
module Pairing
|
|
14
16
|
module_function
|
|
15
17
|
|
|
16
18
|
# Expand each positional source: a directory -> its sorted **/*.rb files
|
|
17
|
-
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
18
|
-
# itself. Flattened, deduped, order-stable.
|
|
19
|
+
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
20
|
+
# -> itself. Flattened, deduped, order-stable.
|
|
21
|
+
#
|
|
22
|
+
# @param args [Array<String>] source paths or directories.
|
|
23
|
+
# @param project_root [String] repository root for relative expansion.
|
|
24
|
+
# @return [Array<String>] flattened, deduped source file list.
|
|
19
25
|
def expand_sources(args, project_root:)
|
|
20
26
|
root = File.expand_path(project_root)
|
|
21
27
|
Array(args).flat_map do |arg|
|
|
@@ -30,8 +36,13 @@ module Mutineer
|
|
|
30
36
|
|
|
31
37
|
# The first EXISTING candidate test path for a source (relative to
|
|
32
38
|
# project_root), or nil. `prefer` is the resolved framework ("minitest" |
|
|
33
|
-
# "rspec"): its candidates are tried first, the other framework's as
|
|
34
|
-
# so a minitest default still finds a spec and vice-versa.
|
|
39
|
+
# "rspec"): its candidates are tried first, the other framework's as
|
|
40
|
+
# fallback, so a minitest default still finds a spec and vice-versa.
|
|
41
|
+
#
|
|
42
|
+
# @param source_rel [String] relative source path.
|
|
43
|
+
# @param project_root [String] repository root for existence checks.
|
|
44
|
+
# @param prefer [String] preferred framework name.
|
|
45
|
+
# @return [String, nil] existing test path or nil.
|
|
35
46
|
def infer_test(source_rel, project_root:, prefer: "minitest")
|
|
36
47
|
base, lib = logical_path(source_rel)
|
|
37
48
|
candidates(base, lib, prefer).find do |rel|
|
|
@@ -39,10 +50,13 @@ module Mutineer
|
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
43
|
-
# app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
|
|
44
|
-
# path minus ".rb" (still attempted). Namespaced subdirs are
|
|
45
|
-
# verbatim — structural, never constant resolution.
|
|
53
|
+
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
54
|
+
# sources. app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
|
|
55
|
+
# else -> the path minus ".rb" (still attempted). Namespaced subdirs are
|
|
56
|
+
# preserved verbatim — structural, never constant resolution.
|
|
57
|
+
#
|
|
58
|
+
# @param source_rel [String] relative source path.
|
|
59
|
+
# @return [Array(String, Boolean)] logical base path and whether it came from lib/.
|
|
46
60
|
def logical_path(source_rel)
|
|
47
61
|
no_ext = source_rel.sub(/\.rb\z/, "")
|
|
48
62
|
if no_ext.start_with?("app/")
|
|
@@ -56,6 +70,11 @@ module Mutineer
|
|
|
56
70
|
|
|
57
71
|
# Ordered candidate test paths. lib/ sources also get test/lib/... and
|
|
58
72
|
# spec/lib/... (Rails apps put lib tests under either layout).
|
|
73
|
+
#
|
|
74
|
+
# @param base [String] logical source path without extension.
|
|
75
|
+
# @param lib [Boolean] whether the source originated from lib/.
|
|
76
|
+
# @param prefer [String] preferred framework name.
|
|
77
|
+
# @return [Array<String>] candidate test paths in preferred order.
|
|
59
78
|
def candidates(base, lib, prefer)
|
|
60
79
|
minitest = ["test/#{base}_test.rb"]
|
|
61
80
|
minitest << "test/lib/#{base}_test.rb" if lib
|
data/lib/mutineer/parser.rb
CHANGED
|
@@ -3,22 +3,32 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
module Mutineer
|
|
6
|
-
# Raised only for I/O failures while reading a source file.
|
|
7
|
-
#
|
|
6
|
+
# Raised only for I/O failures while reading a source file.
|
|
7
|
+
#
|
|
8
|
+
# Prism syntax errors are NOT raised — they are in-band via ParseResult#errors.
|
|
8
9
|
class ParseError < StandardError; end
|
|
9
10
|
|
|
10
|
-
# Thin boundary around Prism.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# Thin boundary around Prism.
|
|
12
|
+
#
|
|
13
|
+
# Both methods return a Prism::ParseResult so all callers use result.value,
|
|
14
|
+
# result.source.source (raw bytes), and result.errors uniformly. No wrapping
|
|
15
|
+
# struct.
|
|
13
16
|
class Parser
|
|
14
|
-
#
|
|
17
|
+
# Parses a file with Prism.
|
|
18
|
+
#
|
|
19
|
+
# @param path [String] source file path.
|
|
20
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
21
|
+
# @raise [Mutineer::ParseError] when file I/O fails.
|
|
15
22
|
def self.parse_file(path)
|
|
16
23
|
Prism.parse_file(path)
|
|
17
24
|
rescue SystemCallError => e
|
|
18
25
|
raise ParseError, e.message
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
#
|
|
28
|
+
# Parses source text with Prism.
|
|
29
|
+
#
|
|
30
|
+
# @param source [String] source text.
|
|
31
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
22
32
|
def self.parse_string(source)
|
|
23
33
|
Prism.parse(source)
|
|
24
34
|
end
|
data/lib/mutineer/project.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require "set"
|
|
4
5
|
require_relative "parser"
|
|
5
6
|
require_relative "subject"
|
|
6
7
|
|
|
@@ -8,12 +9,17 @@ module Mutineer
|
|
|
8
9
|
# Subject discovery: parse each path and walk its AST for method definitions,
|
|
9
10
|
# tracking the enclosing class/module namespace.
|
|
10
11
|
class Project
|
|
11
|
-
#
|
|
12
|
+
# Discovers subjects from source paths.
|
|
13
|
+
#
|
|
14
|
+
# @param paths [Array<String>] source file paths.
|
|
15
|
+
# @param only [String, nil] optional qualified-name filter.
|
|
16
|
+
# @return [Array<Mutineer::Subject>] discovered subjects.
|
|
12
17
|
def self.discover(paths, only: nil)
|
|
13
18
|
subjects = Array(paths).flat_map do |path|
|
|
14
19
|
result = Parser.parse_file(path)
|
|
15
20
|
visitor = SubjectVisitor.new(path)
|
|
16
21
|
visitor.visit(result.value)
|
|
22
|
+
visitor.promote_module_functions!
|
|
17
23
|
visitor.subjects
|
|
18
24
|
end
|
|
19
25
|
only ? subjects.select { |s| s.qualified_name == only } : subjects
|
|
@@ -24,31 +30,87 @@ module Mutineer
|
|
|
24
30
|
class SubjectVisitor < Prism::Visitor
|
|
25
31
|
attr_reader :subjects
|
|
26
32
|
|
|
33
|
+
# Builds a subject visitor.
|
|
34
|
+
#
|
|
35
|
+
# @param file [String] source file path being visited.
|
|
27
36
|
def initialize(file)
|
|
28
37
|
@file = file
|
|
29
38
|
@namespace_stack = []
|
|
30
39
|
@subjects = []
|
|
31
40
|
@singleton_depth = 0
|
|
41
|
+
@module_function_active = false # bareword `module_function` seen in this module body
|
|
42
|
+
@module_function_names = [] # names from `module_function :a, :b` / `module_function def`
|
|
32
43
|
super()
|
|
33
44
|
end
|
|
34
45
|
|
|
46
|
+
# Promote `module_function :name` / `module_function def name` subjects to
|
|
47
|
+
# singleton after the full walk — the naming call may appear before or after
|
|
48
|
+
# the def, so it can't be decided at visit_def_node time (#20).
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
def promote_module_functions!
|
|
52
|
+
return if @module_function_names.empty?
|
|
53
|
+
|
|
54
|
+
names = @module_function_names.to_set
|
|
55
|
+
@subjects.each { |s| s.singleton = true if names.include?(s.name) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Visits class nodes and tracks namespace nesting.
|
|
59
|
+
#
|
|
60
|
+
# @param node [Prism::ClassNode] class node.
|
|
61
|
+
# @return [void]
|
|
35
62
|
def visit_class_node(node)
|
|
36
63
|
@namespace_stack.push(extract_constant_name(node.constant_path))
|
|
64
|
+
saved = @module_function_active
|
|
65
|
+
@module_function_active = false # module_function state does not cross a class boundary
|
|
37
66
|
super
|
|
67
|
+
@module_function_active = saved
|
|
38
68
|
@namespace_stack.pop
|
|
39
69
|
end
|
|
40
70
|
|
|
71
|
+
# Visits module nodes and tracks namespace nesting.
|
|
72
|
+
#
|
|
73
|
+
# @param node [Prism::ModuleNode] module node.
|
|
74
|
+
# @return [void]
|
|
41
75
|
def visit_module_node(node)
|
|
42
76
|
@namespace_stack.push(extract_constant_name(node.constant_path))
|
|
77
|
+
saved = @module_function_active
|
|
78
|
+
@module_function_active = false # each module body starts without module_function active
|
|
43
79
|
super
|
|
80
|
+
@module_function_active = saved
|
|
44
81
|
@namespace_stack.pop
|
|
45
82
|
end
|
|
46
83
|
|
|
84
|
+
# Track `module_function` so its methods are recorded as singletons (#20) —
|
|
85
|
+
# the called form is the singleton method on the module object. Bareword
|
|
86
|
+
# `module_function` flips all SUBSEQUENT defs in this body; the argument
|
|
87
|
+
# forms (`:sym`, `def`) name methods promoted after the walk.
|
|
88
|
+
#
|
|
89
|
+
# @param node [Prism::CallNode] call node.
|
|
90
|
+
# @return [void]
|
|
91
|
+
def visit_call_node(node)
|
|
92
|
+
if node.name == :module_function && node.receiver.nil?
|
|
93
|
+
args = node.arguments&.arguments || []
|
|
94
|
+
if args.empty?
|
|
95
|
+
@module_function_active = true
|
|
96
|
+
else
|
|
97
|
+
args.each do |arg|
|
|
98
|
+
@module_function_names << arg.value.to_sym if arg.is_a?(Prism::SymbolNode)
|
|
99
|
+
@module_function_names << arg.name if arg.is_a?(Prism::DefNode)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
47
106
|
# Methods inside `class << self` are class methods of the enclosing
|
|
48
107
|
# namespace, but their def nodes have no receiver — track the singleton
|
|
49
108
|
# context so they're recorded as singleton (so redefine targets the
|
|
50
109
|
# singleton_class, not instances). `class << some_other_obj` can't be
|
|
51
110
|
# represented against the namespace, so its defs are skipped (not recursed).
|
|
111
|
+
#
|
|
112
|
+
# @param node [Prism::SingletonClassNode] singleton-class node.
|
|
113
|
+
# @return [void]
|
|
52
114
|
def visit_singleton_class_node(node)
|
|
53
115
|
return unless node.expression.is_a?(Prism::SelfNode)
|
|
54
116
|
|
|
@@ -57,12 +119,16 @@ module Mutineer
|
|
|
57
119
|
@singleton_depth -= 1
|
|
58
120
|
end
|
|
59
121
|
|
|
122
|
+
# Records a discovered method definition.
|
|
123
|
+
#
|
|
124
|
+
# @param node [Prism::DefNode] method definition node.
|
|
125
|
+
# @return [void]
|
|
60
126
|
def visit_def_node(node)
|
|
61
127
|
@subjects << Subject.new(
|
|
62
128
|
file: @file,
|
|
63
129
|
namespace: @namespace_stack.dup,
|
|
64
130
|
name: node.name,
|
|
65
|
-
singleton: !node.receiver.nil? || @singleton_depth.positive
|
|
131
|
+
singleton: !node.receiver.nil? || @singleton_depth.positive? || @module_function_active,
|
|
66
132
|
def_node: node
|
|
67
133
|
)
|
|
68
134
|
super
|
|
@@ -70,6 +136,11 @@ module Mutineer
|
|
|
70
136
|
|
|
71
137
|
private
|
|
72
138
|
|
|
139
|
+
# Extracts a constant name from a Prism constant node.
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
# @param node [Prism::Node] constant node.
|
|
143
|
+
# @return [String, nil] constant name.
|
|
73
144
|
def extract_constant_name(node)
|
|
74
145
|
case node
|
|
75
146
|
when Prism::ConstantReadNode
|
data/lib/mutineer/reporter.rb
CHANGED
|
@@ -41,6 +41,12 @@ module Mutineer
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Renders the human report.
|
|
45
|
+
#
|
|
46
|
+
# @param out [IO] output stream.
|
|
47
|
+
# @param err [IO] error stream.
|
|
48
|
+
# @param threshold [Float] score threshold.
|
|
49
|
+
# @return [void]
|
|
44
50
|
def human_report(out, err, threshold)
|
|
45
51
|
if @agg.total.zero?
|
|
46
52
|
err.puts "No mutations generated — verify target files contain in-scope " \
|
|
@@ -75,6 +81,11 @@ module Mutineer
|
|
|
75
81
|
# Canonical machine-readable schema (KTD7). survivors/no_coverage are sorted
|
|
76
82
|
# by (file, line, operator) so output is byte-stable regardless of --jobs
|
|
77
83
|
# worker finish order (R22).
|
|
84
|
+
# Renders the JSON report.
|
|
85
|
+
#
|
|
86
|
+
# @api private
|
|
87
|
+
# @param baseline [Mutineer::Baseline::Delta, nil] baseline delta.
|
|
88
|
+
# @return [String] JSON text.
|
|
78
89
|
def json_report(baseline = nil)
|
|
79
90
|
killed = @agg.killed_count
|
|
80
91
|
survived = @agg.survived_count
|
|
@@ -135,6 +146,12 @@ module Mutineer
|
|
|
135
146
|
}
|
|
136
147
|
end
|
|
137
148
|
|
|
149
|
+
# Builds per-source JSON.
|
|
150
|
+
#
|
|
151
|
+
# @api private
|
|
152
|
+
# @param file [String] source file path.
|
|
153
|
+
# @param agg [Mutineer::AggregateResult] source aggregate.
|
|
154
|
+
# @return [Hash] per-source JSON object.
|
|
138
155
|
def per_source_json(file, agg)
|
|
139
156
|
{
|
|
140
157
|
file: file, total: agg.total,
|
|
@@ -143,6 +160,11 @@ module Mutineer
|
|
|
143
160
|
}
|
|
144
161
|
end
|
|
145
162
|
|
|
163
|
+
# Builds survivor JSON.
|
|
164
|
+
#
|
|
165
|
+
# @api private
|
|
166
|
+
# @param result [Mutineer::Result] survivor result.
|
|
167
|
+
# @return [Hash] survivor JSON object.
|
|
146
168
|
def survivor_json(result)
|
|
147
169
|
m = result.mutation
|
|
148
170
|
file = result.subject.file
|
|
@@ -198,6 +220,11 @@ module Mutineer
|
|
|
198
220
|
[start_line, original_block, mutated_block, token]
|
|
199
221
|
end
|
|
200
222
|
|
|
223
|
+
# Builds no-coverage JSON.
|
|
224
|
+
#
|
|
225
|
+
# @api private
|
|
226
|
+
# @param result [Mutineer::Result] result object.
|
|
227
|
+
# @return [Hash] no-coverage JSON object.
|
|
201
228
|
def no_coverage_json(result)
|
|
202
229
|
m = result.mutation
|
|
203
230
|
file = result.subject.file
|
|
@@ -209,6 +236,10 @@ module Mutineer
|
|
|
209
236
|
}
|
|
210
237
|
end
|
|
211
238
|
|
|
239
|
+
# Writes the summary block.
|
|
240
|
+
#
|
|
241
|
+
# @param out [IO] output stream.
|
|
242
|
+
# @return [void]
|
|
212
243
|
def summary(out)
|
|
213
244
|
out.puts "Summary"
|
|
214
245
|
out.puts "-------"
|
|
@@ -222,6 +253,11 @@ module Mutineer
|
|
|
222
253
|
out.puts format("Ignored: %-6d (equivalent, suppressed)", @agg.ignored_count)
|
|
223
254
|
end
|
|
224
255
|
|
|
256
|
+
# Writes the score line.
|
|
257
|
+
#
|
|
258
|
+
# @param out [IO] output stream.
|
|
259
|
+
# @param err [IO] error stream.
|
|
260
|
+
# @return [void]
|
|
225
261
|
def score_line(out, err)
|
|
226
262
|
score = @agg.mutation_score
|
|
227
263
|
excluded = "#{@agg.no_coverage_count} no-coverage, #{@agg.uncapturable_count} uncapturable, " \
|
|
@@ -239,6 +275,10 @@ module Mutineer
|
|
|
239
275
|
# #11: one line per source after the global summary, so a multi-source run
|
|
240
276
|
# shows which file is weak. Omitted for a single-source run — the global
|
|
241
277
|
# summary already says everything (ponytail: no redundant one-line block).
|
|
278
|
+
# Writes the per-source breakdown.
|
|
279
|
+
#
|
|
280
|
+
# @param out [IO] output stream.
|
|
281
|
+
# @return [void]
|
|
242
282
|
def per_source(out)
|
|
243
283
|
sources = @agg.by_source
|
|
244
284
|
return if sources.size <= 1
|
|
@@ -274,6 +314,10 @@ module Mutineer
|
|
|
274
314
|
out.puts(delta.regressed ? "REGRESSION vs baseline" : "OK: no regression vs baseline")
|
|
275
315
|
end
|
|
276
316
|
|
|
317
|
+
# Writes the survivors block.
|
|
318
|
+
#
|
|
319
|
+
# @param out [IO] output stream.
|
|
320
|
+
# @return [void]
|
|
277
321
|
def survivors(out)
|
|
278
322
|
mutants = @agg.surviving_mutants
|
|
279
323
|
return if mutants.empty?
|
|
@@ -288,6 +332,12 @@ module Mutineer
|
|
|
288
332
|
end
|
|
289
333
|
end
|
|
290
334
|
|
|
335
|
+
# Writes one survivor entry.
|
|
336
|
+
#
|
|
337
|
+
# @param out [IO] output stream.
|
|
338
|
+
# @param file [String] source file path.
|
|
339
|
+
# @param result [Mutineer::Result] survivor result.
|
|
340
|
+
# @return [void]
|
|
291
341
|
def survivor(out, file, result)
|
|
292
342
|
m = result.mutation
|
|
293
343
|
source = @source_map[file] || File.read(file)
|
|
@@ -299,6 +349,11 @@ module Mutineer
|
|
|
299
349
|
mutated_block.each_line { |l| out.puts " + #{l.chomp}" }
|
|
300
350
|
end
|
|
301
351
|
|
|
352
|
+
# Writes the final verdict line.
|
|
353
|
+
#
|
|
354
|
+
# @param out [IO] output stream.
|
|
355
|
+
# @param threshold [Float] score threshold.
|
|
356
|
+
# @return [void]
|
|
302
357
|
def verdict(out, threshold)
|
|
303
358
|
score = @agg.mutation_score
|
|
304
359
|
return if score.nil?
|