evilution 0.19.0 → 0.20.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/.beads/.migration-hint-ts +1 -1
- data/.beads/issues.jsonl +22 -22
- data/CHANGELOG.md +18 -0
- data/README.md +22 -3
- data/lib/evilution/equivalent/detector.rb +3 -1
- data/lib/evilution/equivalent/heuristic/alias_swap.rb +2 -1
- data/lib/evilution/equivalent/heuristic/void_context.rb +77 -0
- data/lib/evilution/integration/crash_detector.rb +55 -0
- data/lib/evilution/integration/rspec.rb +32 -5
- data/lib/evilution/mutator/operator/begin_unwrap.rb +21 -0
- data/lib/evilution/mutator/operator/block_param_removal.rb +57 -0
- data/lib/evilution/mutator/operator/case_when.rb +55 -0
- data/lib/evilution/mutator/operator/equality_to_identity.rb +22 -0
- data/lib/evilution/mutator/operator/lambda_body.rb +18 -0
- data/lib/evilution/mutator/operator/loop_flip.rb +27 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +10 -6
- data/lib/evilution/mutator/operator/predicate_replacement.rb +27 -0
- data/lib/evilution/mutator/operator/retry_removal.rb +16 -0
- data/lib/evilution/mutator/operator/send_mutation.rb +8 -1
- data/lib/evilution/mutator/operator/string_interpolation.rb +32 -0
- data/lib/evilution/mutator/registry.rb +10 -1
- data/lib/evilution/related_spec_heuristic.rb +63 -0
- data/lib/evilution/reporter/cli.rb +14 -8
- data/lib/evilution/reporter/html.rb +32 -2
- data/lib/evilution/reporter/json.rb +14 -0
- data/lib/evilution/result/coverage_gap.rb +35 -0
- data/lib/evilution/result/coverage_gap_grouper.rb +22 -0
- data/lib/evilution/result/summary.rb +5 -0
- data/lib/evilution/session/store.rb +13 -0
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +9 -0
- metadata +16 -2
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::BlockParamRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_def_node(node)
|
|
7
|
+
return super unless node.parameters
|
|
8
|
+
return super unless node.parameters.block
|
|
9
|
+
|
|
10
|
+
if only_block_param?(node.parameters)
|
|
11
|
+
remove_entire_params(node)
|
|
12
|
+
else
|
|
13
|
+
remove_block_param(node)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def only_block_param?(params)
|
|
22
|
+
params.requireds.empty? &&
|
|
23
|
+
params.optionals.empty? &&
|
|
24
|
+
params.keywords.empty? &&
|
|
25
|
+
params.rest.nil? &&
|
|
26
|
+
params.keyword_rest.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_entire_params(node)
|
|
30
|
+
start_offset = node.lparen_loc.start_offset
|
|
31
|
+
end_offset = node.rparen_loc.start_offset + node.rparen_loc.length
|
|
32
|
+
add_mutation(
|
|
33
|
+
offset: start_offset,
|
|
34
|
+
length: end_offset - start_offset,
|
|
35
|
+
replacement: "",
|
|
36
|
+
node: node
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_block_param(node)
|
|
41
|
+
block_loc = node.parameters.block.location
|
|
42
|
+
params_text = @file_source.byteslice(node.parameters.location.start_offset, node.parameters.location.length)
|
|
43
|
+
block_rel = block_loc.start_offset - node.parameters.location.start_offset
|
|
44
|
+
|
|
45
|
+
# Find the comma before the block param and remove ", &block"
|
|
46
|
+
comma_pos = params_text.rindex(",", block_rel - 1)
|
|
47
|
+
remove_start = node.parameters.location.start_offset + comma_pos
|
|
48
|
+
remove_end = block_loc.start_offset + block_loc.length
|
|
49
|
+
|
|
50
|
+
add_mutation(
|
|
51
|
+
offset: remove_start,
|
|
52
|
+
length: remove_end - remove_start,
|
|
53
|
+
replacement: "",
|
|
54
|
+
node: node
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::CaseWhen < Evilution::Mutator::Base
|
|
6
|
+
def visit_case_node(node)
|
|
7
|
+
remove_when_branches(node)
|
|
8
|
+
replace_when_bodies(node)
|
|
9
|
+
remove_else_branch(node)
|
|
10
|
+
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def remove_when_branches(node)
|
|
17
|
+
return if node.conditions.length < 2
|
|
18
|
+
|
|
19
|
+
node.conditions.each do |when_node|
|
|
20
|
+
add_mutation(
|
|
21
|
+
offset: when_node.location.start_offset,
|
|
22
|
+
length: when_node.location.length,
|
|
23
|
+
replacement: "",
|
|
24
|
+
node: when_node
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def replace_when_bodies(node)
|
|
30
|
+
node.conditions.each do |when_node|
|
|
31
|
+
next if when_node.statements.nil? || when_node.statements.body.empty?
|
|
32
|
+
|
|
33
|
+
add_mutation(
|
|
34
|
+
offset: when_node.statements.location.start_offset,
|
|
35
|
+
length: when_node.statements.location.length,
|
|
36
|
+
replacement: "nil",
|
|
37
|
+
node: when_node
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def remove_else_branch(node)
|
|
43
|
+
return if node.else_clause.nil?
|
|
44
|
+
return if node.else_clause.statements.nil?
|
|
45
|
+
|
|
46
|
+
start_offset = node.else_clause.else_keyword_loc.start_offset
|
|
47
|
+
end_offset = node.else_clause.statements.location.start_offset + node.else_clause.statements.location.length
|
|
48
|
+
add_mutation(
|
|
49
|
+
offset: start_offset,
|
|
50
|
+
length: end_offset - start_offset,
|
|
51
|
+
replacement: "",
|
|
52
|
+
node: node.else_clause
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::EqualityToIdentity < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
7
|
+
if node.name == :== && node.receiver && node.arguments
|
|
8
|
+
receiver_text = @file_source.byteslice(node.receiver.location.start_offset, node.receiver.location.length)
|
|
9
|
+
arg = node.arguments.arguments.first
|
|
10
|
+
arg_text = @file_source.byteslice(arg.location.start_offset, arg.location.length)
|
|
11
|
+
|
|
12
|
+
add_mutation(
|
|
13
|
+
offset: node.location.start_offset,
|
|
14
|
+
length: node.location.length,
|
|
15
|
+
replacement: "#{receiver_text}.equal?(#{arg_text})",
|
|
16
|
+
node: node
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::LambdaBody < Evilution::Mutator::Base
|
|
6
|
+
def visit_lambda_node(node)
|
|
7
|
+
if node.body
|
|
8
|
+
add_mutation(
|
|
9
|
+
offset: node.body.location.start_offset,
|
|
10
|
+
length: node.body.location.length,
|
|
11
|
+
replacement: "nil",
|
|
12
|
+
node: node
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::LoopFlip < Evilution::Mutator::Base
|
|
6
|
+
def visit_while_node(node)
|
|
7
|
+
add_mutation(
|
|
8
|
+
offset: node.keyword_loc.start_offset,
|
|
9
|
+
length: node.keyword_loc.length,
|
|
10
|
+
replacement: "until",
|
|
11
|
+
node: node
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def visit_until_node(node)
|
|
18
|
+
add_mutation(
|
|
19
|
+
offset: node.keyword_loc.start_offset,
|
|
20
|
+
length: node.keyword_loc.length,
|
|
21
|
+
replacement: "while",
|
|
22
|
+
node: node
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
require_relative "../operator"
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::MethodBodyReplacement < Evilution::Mutator::Base
|
|
6
|
+
REPLACEMENTS = %w[nil self super].freeze
|
|
7
|
+
|
|
6
8
|
def visit_def_node(node)
|
|
7
9
|
if node.body
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
REPLACEMENTS.each do |replacement|
|
|
11
|
+
add_mutation(
|
|
12
|
+
offset: node.body.location.start_offset,
|
|
13
|
+
length: node.body.location.length,
|
|
14
|
+
replacement: replacement,
|
|
15
|
+
node: node
|
|
16
|
+
)
|
|
17
|
+
end
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
super
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::PredicateReplacement < Evilution::Mutator::Base
|
|
6
|
+
def visit_call_node(node)
|
|
7
|
+
if node.name.to_s.end_with?("?")
|
|
8
|
+
loc = node.location
|
|
9
|
+
|
|
10
|
+
add_mutation(
|
|
11
|
+
offset: loc.start_offset,
|
|
12
|
+
length: loc.length,
|
|
13
|
+
replacement: "true",
|
|
14
|
+
node: node
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
add_mutation(
|
|
18
|
+
offset: loc.start_offset,
|
|
19
|
+
length: loc.length,
|
|
20
|
+
replacement: "false",
|
|
21
|
+
node: node
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::RetryRemoval < Evilution::Mutator::Base
|
|
6
|
+
def visit_retry_node(node)
|
|
7
|
+
add_mutation(
|
|
8
|
+
offset: node.location.start_offset,
|
|
9
|
+
length: node.location.length,
|
|
10
|
+
replacement: "nil",
|
|
11
|
+
node: node
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -29,7 +29,14 @@ class Evilution::Mutator::Operator::SendMutation < Evilution::Mutator::Base
|
|
|
29
29
|
to_i: [:to_s],
|
|
30
30
|
to_f: [:to_i],
|
|
31
31
|
to_a: [:to_h],
|
|
32
|
-
to_h: [:to_a]
|
|
32
|
+
to_h: [:to_a],
|
|
33
|
+
downcase: [:upcase],
|
|
34
|
+
upcase: [:downcase],
|
|
35
|
+
strip: %i[lstrip rstrip],
|
|
36
|
+
lstrip: [:strip],
|
|
37
|
+
rstrip: [:strip],
|
|
38
|
+
chomp: [:chop],
|
|
39
|
+
chop: [:chomp]
|
|
33
40
|
}.freeze
|
|
34
41
|
|
|
35
42
|
def visit_call_node(node)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../operator"
|
|
4
|
+
|
|
5
|
+
class Evilution::Mutator::Operator::StringInterpolation < Evilution::Mutator::Base
|
|
6
|
+
def visit_interpolated_string_node(node)
|
|
7
|
+
mutate_embedded_statements(node)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def visit_interpolated_symbol_node(node)
|
|
12
|
+
mutate_embedded_statements(node)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def mutate_embedded_statements(node)
|
|
19
|
+
node.parts.each do |part|
|
|
20
|
+
next unless part.is_a?(Prism::EmbeddedStatementsNode)
|
|
21
|
+
next if part.statements.nil? || part.statements.body.empty?
|
|
22
|
+
|
|
23
|
+
stmt = part.statements
|
|
24
|
+
add_mutation(
|
|
25
|
+
offset: stmt.location.start_offset,
|
|
26
|
+
length: stmt.location.length,
|
|
27
|
+
replacement: "nil",
|
|
28
|
+
node: part
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -65,7 +65,16 @@ class Evilution::Mutator::Registry
|
|
|
65
65
|
Evilution::Mutator::Operator::YieldStatement,
|
|
66
66
|
Evilution::Mutator::Operator::SplatOperator,
|
|
67
67
|
Evilution::Mutator::Operator::DefinedCheck,
|
|
68
|
-
Evilution::Mutator::Operator::RegexCapture
|
|
68
|
+
Evilution::Mutator::Operator::RegexCapture,
|
|
69
|
+
Evilution::Mutator::Operator::LoopFlip,
|
|
70
|
+
Evilution::Mutator::Operator::StringInterpolation,
|
|
71
|
+
Evilution::Mutator::Operator::RetryRemoval,
|
|
72
|
+
Evilution::Mutator::Operator::CaseWhen,
|
|
73
|
+
Evilution::Mutator::Operator::PredicateReplacement,
|
|
74
|
+
Evilution::Mutator::Operator::EqualityToIdentity,
|
|
75
|
+
Evilution::Mutator::Operator::LambdaBody,
|
|
76
|
+
Evilution::Mutator::Operator::BeginUnwrap,
|
|
77
|
+
Evilution::Mutator::Operator::BlockParamRemoval
|
|
69
78
|
].each { |op| registry.register(op) }
|
|
70
79
|
registry
|
|
71
80
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Evilution::RelatedSpecHeuristic
|
|
4
|
+
RELATED_SPEC_DIRS = %w[
|
|
5
|
+
spec/requests
|
|
6
|
+
spec/integration
|
|
7
|
+
spec/features
|
|
8
|
+
spec/system
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
INCLUDES_PATTERN = /\bincludes\(/
|
|
12
|
+
|
|
13
|
+
def call(mutation)
|
|
14
|
+
return [] unless includes_mutation?(mutation)
|
|
15
|
+
|
|
16
|
+
domain = extract_domain(mutation.file_path)
|
|
17
|
+
return [] unless domain
|
|
18
|
+
|
|
19
|
+
find_related_specs(domain)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def includes_mutation?(mutation)
|
|
25
|
+
diff = mutation.diff
|
|
26
|
+
return false unless diff
|
|
27
|
+
|
|
28
|
+
diff.split("\n").any? { |line| line.start_with?("- ") && line.match?(INCLUDES_PATTERN) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def extract_domain(file_path)
|
|
32
|
+
normalized = normalize_path(file_path)
|
|
33
|
+
|
|
34
|
+
# Strip common prefixes and get the relative path under app/ or lib/
|
|
35
|
+
relative = normalized
|
|
36
|
+
.delete_prefix("app/controllers/")
|
|
37
|
+
.delete_prefix("app/models/")
|
|
38
|
+
.delete_prefix("app/")
|
|
39
|
+
.delete_prefix("lib/")
|
|
40
|
+
|
|
41
|
+
# Remove .rb extension and _controller suffix
|
|
42
|
+
basename = relative.sub(/\.rb\z/, "")
|
|
43
|
+
basename = basename.sub(/_controller\z/, "")
|
|
44
|
+
|
|
45
|
+
basename.empty? ? nil : basename
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def normalize_path(path)
|
|
49
|
+
path = path.delete_prefix("./")
|
|
50
|
+
path = path.delete_prefix("#{Dir.pwd}/") if path.start_with?("/")
|
|
51
|
+
path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_related_specs(domain)
|
|
55
|
+
RELATED_SPEC_DIRS.flat_map { |dir| find_specs_in_dir(dir, domain) }.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_specs_in_dir(dir, domain)
|
|
59
|
+
return [] unless Dir.exist?(dir)
|
|
60
|
+
|
|
61
|
+
Dir.glob(File.join(dir, "**", "#{domain}_spec.rb"))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -30,11 +30,12 @@ class Evilution::Reporter::CLI
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def append_survived(lines, summary)
|
|
33
|
-
|
|
33
|
+
gaps = summary.coverage_gaps
|
|
34
|
+
return unless gaps.any?
|
|
34
35
|
|
|
35
36
|
lines << ""
|
|
36
|
-
lines << "Survived mutations:"
|
|
37
|
-
|
|
37
|
+
lines << "Survived mutations (#{gaps.length} coverage gap#{"s" unless gaps.length == 1}):"
|
|
38
|
+
gaps.each { |gap| lines << format_coverage_gap(gap) }
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def append_neutral(lines, summary)
|
|
@@ -90,11 +91,16 @@ class Evilution::Reporter::CLI
|
|
|
90
91
|
"Efficiency: #{pct} killtime, #{rate} mutations/s"
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
def format_coverage_gap(gap)
|
|
95
|
+
location = "#{gap.file_path}:#{gap.line}"
|
|
96
|
+
header = if gap.single?
|
|
97
|
+
" #{gap.primary_operator}: #{location} (#{gap.subject_name})"
|
|
98
|
+
else
|
|
99
|
+
operators = gap.operator_names.join(", ")
|
|
100
|
+
" #{location} (#{gap.subject_name}) [#{gap.count} mutations: #{operators}]"
|
|
101
|
+
end
|
|
102
|
+
diff_lines = gap.primary_diff.split("\n").map { |l| " #{l}" }.join("\n")
|
|
103
|
+
"#{header}\n#{diff_lines}"
|
|
98
104
|
end
|
|
99
105
|
|
|
100
106
|
def format_neutral(result)
|
|
@@ -4,6 +4,7 @@ require "cgi"
|
|
|
4
4
|
require_relative "suggestion"
|
|
5
5
|
|
|
6
6
|
require_relative "../reporter"
|
|
7
|
+
require_relative "../result/coverage_gap_grouper"
|
|
7
8
|
|
|
8
9
|
class Evilution::Reporter::HTML
|
|
9
10
|
def initialize(baseline: nil)
|
|
@@ -156,15 +157,39 @@ class Evilution::Reporter::HTML
|
|
|
156
157
|
def build_survived_details(survived)
|
|
157
158
|
return "" if survived.empty?
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
gaps = Evilution::Result::CoverageGapGrouper.new.call(survived)
|
|
161
|
+
entries = gaps.map { |gap| build_gap_entry(gap) }.join("\n")
|
|
160
162
|
<<~HTML
|
|
161
163
|
<div class="survived-details">
|
|
162
|
-
<h3>
|
|
164
|
+
<h3>Coverage Gaps (#{gaps.length})</h3>
|
|
163
165
|
#{entries}
|
|
164
166
|
</div>
|
|
165
167
|
HTML
|
|
166
168
|
end
|
|
167
169
|
|
|
170
|
+
def build_gap_entry(gap)
|
|
171
|
+
if gap.single?
|
|
172
|
+
build_survived_entry(gap.mutation_results.first)
|
|
173
|
+
else
|
|
174
|
+
build_grouped_gap_entry(gap)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def build_grouped_gap_entry(gap)
|
|
179
|
+
operator_tags = gap.operator_names.map { |op| %(<span class="operator-tag">#{h(op)}</span>) }.join(" ")
|
|
180
|
+
entries_html = gap.mutation_results.map { |r| build_survived_entry(r) }.join("\n")
|
|
181
|
+
<<~HTML
|
|
182
|
+
<div class="coverage-gap">
|
|
183
|
+
<div class="gap-header">
|
|
184
|
+
<span class="location">#{h(gap.file_path)}:#{gap.line} (#{h(gap.subject_name)})</span>
|
|
185
|
+
<span class="gap-count">#{gap.count} mutations</span>
|
|
186
|
+
#{operator_tags}
|
|
187
|
+
</div>
|
|
188
|
+
#{entries_html}
|
|
189
|
+
</div>
|
|
190
|
+
HTML
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
def build_survived_entry(result)
|
|
169
194
|
mutation = result.mutation
|
|
170
195
|
suggestion_text = @suggestion.suggestion_for(mutation)
|
|
@@ -307,6 +332,11 @@ class Evilution::Reporter::HTML
|
|
|
307
332
|
.diff-removed { color: #f85149; display: block; }
|
|
308
333
|
.diff-added { color: #3fb950; display: block; }
|
|
309
334
|
.suggestion { color: #d29922; font-size: 0.8rem; margin-top: 0.5rem; font-style: italic; }
|
|
335
|
+
.coverage-gap { border: 1px solid #30363d; border-radius: 6px; padding: 0.75rem; margin-bottom: 0.75rem; background: #161b22; }
|
|
336
|
+
.gap-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; font-size: 0.85rem; padding-bottom: 0.5rem; border-bottom: 1px solid #21262d; }
|
|
337
|
+
.gap-header .location { color: #58a6ff; font-family: monospace; }
|
|
338
|
+
.gap-count { background: #4a1a1a; color: #f85149; font-size: 0.7rem; padding: 0.1rem 0.5rem; border-radius: 10px; font-weight: bold; }
|
|
339
|
+
.operator-tag { background: #21262d; color: #8b949e; font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 4px; font-family: monospace; }
|
|
310
340
|
.empty { color: #8b949e; text-align: center; padding: 2rem; }
|
|
311
341
|
.baseline-comparison { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 2rem; }
|
|
312
342
|
.baseline-comparison h2 { font-size: 1rem; color: #f0f6fc; margin-bottom: 0.75rem; }
|
|
@@ -24,6 +24,7 @@ class Evilution::Reporter::JSON
|
|
|
24
24
|
timestamp: Time.now.iso8601,
|
|
25
25
|
summary: build_summary(summary),
|
|
26
26
|
survived: summary.survived_results.map { |r| build_mutation_detail(r) },
|
|
27
|
+
coverage_gaps: build_coverage_gaps(summary),
|
|
27
28
|
killed: summary.killed_results.map { |r| build_mutation_detail(r) },
|
|
28
29
|
neutral: summary.neutral_results.map { |r| build_mutation_detail(r) },
|
|
29
30
|
timed_out: summary.results.select(&:timeout?).map { |r| build_mutation_detail(r) },
|
|
@@ -81,6 +82,19 @@ class Evilution::Reporter::JSON
|
|
|
81
82
|
detail
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
def build_coverage_gaps(summary)
|
|
86
|
+
summary.coverage_gaps.map do |gap|
|
|
87
|
+
{
|
|
88
|
+
file: gap.file_path,
|
|
89
|
+
subject: gap.subject_name,
|
|
90
|
+
line: gap.line,
|
|
91
|
+
operators: gap.operator_names,
|
|
92
|
+
count: gap.count,
|
|
93
|
+
mutations: gap.mutation_results.map { |r| build_mutation_detail(r) }
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
84
98
|
def build_disabled_detail(mutation)
|
|
85
99
|
{
|
|
86
100
|
operator: mutation.operator_name,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::CoverageGap
|
|
6
|
+
attr_reader :file_path, :subject_name, :line, :mutation_results
|
|
7
|
+
|
|
8
|
+
def initialize(file_path:, subject_name:, line:, mutation_results:)
|
|
9
|
+
@file_path = file_path
|
|
10
|
+
@subject_name = subject_name
|
|
11
|
+
@line = line
|
|
12
|
+
@mutation_results = mutation_results.dup.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def operator_names
|
|
17
|
+
mutation_results.map { |r| r.mutation.operator_name }.uniq
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def primary_operator
|
|
21
|
+
mutation_results.first.mutation.operator_name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def primary_diff
|
|
25
|
+
mutation_results.first.mutation.diff
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def count
|
|
29
|
+
mutation_results.length
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def single?
|
|
33
|
+
count == 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "coverage_gap"
|
|
4
|
+
|
|
5
|
+
class Evilution::Result::CoverageGapGrouper
|
|
6
|
+
def call(survived_results)
|
|
7
|
+
grouped = survived_results.group_by do |r|
|
|
8
|
+
[r.mutation.file_path, r.mutation.subject.name, r.mutation.line]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
gaps = grouped.map do |(file_path, subject_name, line), results|
|
|
12
|
+
Evilution::Result::CoverageGap.new(
|
|
13
|
+
file_path: file_path,
|
|
14
|
+
subject_name: subject_name,
|
|
15
|
+
line: line,
|
|
16
|
+
mutation_results: results
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
gaps.sort_by { |gap| [gap.file_path, gap.line, gap.subject_name] }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../result"
|
|
4
|
+
require_relative "coverage_gap_grouper"
|
|
4
5
|
|
|
5
6
|
class Evilution::Result::Summary
|
|
6
7
|
attr_reader :results, :duration, :skipped, :disabled_mutations
|
|
@@ -73,6 +74,10 @@ class Evilution::Result::Summary
|
|
|
73
74
|
results.select(&:equivalent?)
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def coverage_gaps
|
|
78
|
+
Evilution::Result::CoverageGapGrouper.new.call(survived_results)
|
|
79
|
+
end
|
|
80
|
+
|
|
76
81
|
def killtime
|
|
77
82
|
results.sum(0.0, &:duration)
|
|
78
83
|
end
|
|
@@ -65,6 +65,7 @@ class Evilution::Session::Store
|
|
|
65
65
|
git: git_context,
|
|
66
66
|
summary: build_summary(summary),
|
|
67
67
|
survived: summary.survived_results.map { |r| build_mutation_detail(r) },
|
|
68
|
+
coverage_gaps: build_coverage_gaps(summary),
|
|
68
69
|
killed_count: summary.killed,
|
|
69
70
|
timed_out_count: summary.timed_out,
|
|
70
71
|
error_count: summary.errors,
|
|
@@ -101,6 +102,18 @@ class Evilution::Session::Store
|
|
|
101
102
|
}
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
def build_coverage_gaps(summary)
|
|
106
|
+
summary.coverage_gaps.map do |gap|
|
|
107
|
+
{
|
|
108
|
+
file: gap.file_path,
|
|
109
|
+
subject: gap.subject_name,
|
|
110
|
+
line: gap.line,
|
|
111
|
+
operators: gap.operator_names,
|
|
112
|
+
count: gap.count
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
def git_context
|
|
105
118
|
sha = `git rev-parse HEAD 2>/dev/null`.strip
|
|
106
119
|
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
data/lib/evilution/version.rb
CHANGED
data/lib/evilution.rb
CHANGED
|
@@ -81,6 +81,15 @@ require_relative "evilution/mutator/operator/yield_statement"
|
|
|
81
81
|
require_relative "evilution/mutator/operator/splat_operator"
|
|
82
82
|
require_relative "evilution/mutator/operator/defined_check"
|
|
83
83
|
require_relative "evilution/mutator/operator/regex_capture"
|
|
84
|
+
require_relative "evilution/mutator/operator/loop_flip"
|
|
85
|
+
require_relative "evilution/mutator/operator/string_interpolation"
|
|
86
|
+
require_relative "evilution/mutator/operator/retry_removal"
|
|
87
|
+
require_relative "evilution/mutator/operator/case_when"
|
|
88
|
+
require_relative "evilution/mutator/operator/predicate_replacement"
|
|
89
|
+
require_relative "evilution/mutator/operator/equality_to_identity"
|
|
90
|
+
require_relative "evilution/mutator/operator/lambda_body"
|
|
91
|
+
require_relative "evilution/mutator/operator/begin_unwrap"
|
|
92
|
+
require_relative "evilution/mutator/operator/block_param_removal"
|
|
84
93
|
require_relative "evilution/mutator/registry"
|
|
85
94
|
require_relative "evilution/equivalent"
|
|
86
95
|
require_relative "evilution/equivalent/heuristic"
|