sixth_sense 0.1.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/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/exe/sixth_sense +7 -0
- data/lib/sixth_sense/adapters/minitest.rb +145 -0
- data/lib/sixth_sense/adapters/rspec.rb +373 -0
- data/lib/sixth_sense/adapters/test_unit.rb +142 -0
- data/lib/sixth_sense/analysis_context.rb +35 -0
- data/lib/sixth_sense/analysis_runner.rb +141 -0
- data/lib/sixth_sense/analyzer.rb +85 -0
- data/lib/sixth_sense/analyzers/adequacy_checked_coverage.rb +39 -0
- data/lib/sixth_sense/analyzers/adequacy_coverage.rb +63 -0
- data/lib/sixth_sense/analyzers/adequacy_mutation.rb +141 -0
- data/lib/sixth_sense/analyzers/quality_assertion_density.rb +41 -0
- data/lib/sixth_sense/analyzers/quality_flakiness.rb +53 -0
- data/lib/sixth_sense/analyzers/quality_test_smells.rb +253 -0
- data/lib/sixth_sense/analyzers/redundancy_clone.rb +54 -0
- data/lib/sixth_sense/analyzers/redundancy_coverage.rb +39 -0
- data/lib/sixth_sense/analyzers/redundancy_mutation.rb +39 -0
- data/lib/sixth_sense/analyzers/redundancy_requirement.rb +70 -0
- data/lib/sixth_sense/changed_files.rb +45 -0
- data/lib/sixth_sense/cli.rb +229 -0
- data/lib/sixth_sense/config.rb +91 -0
- data/lib/sixth_sense/engines/mutant.rb +258 -0
- data/lib/sixth_sense/framework_adapter.rb +55 -0
- data/lib/sixth_sense/guardrail/baseline.rb +135 -0
- data/lib/sixth_sense/guardrail/evaluator.rb +69 -0
- data/lib/sixth_sense/model.rb +264 -0
- data/lib/sixth_sense/mutation_cache.rb +93 -0
- data/lib/sixth_sense/mutation_engine.rb +52 -0
- data/lib/sixth_sense/mutation_matrix_mutant_generator.rb +462 -0
- data/lib/sixth_sense/mutation_matrix_producer.rb +308 -0
- data/lib/sixth_sense/mutation_score_cache_writer.rb +75 -0
- data/lib/sixth_sense/rake_task.rb +16 -0
- data/lib/sixth_sense/reporters/console.rb +31 -0
- data/lib/sixth_sense/reporters/html.rb +62 -0
- data/lib/sixth_sense/reporters/json.rb +18 -0
- data/lib/sixth_sense/reporters/markdown.rb +34 -0
- data/lib/sixth_sense/reporters/sarif.rb +77 -0
- data/lib/sixth_sense/result.rb +86 -0
- data/lib/sixth_sense/runners/checked_coverage_estimator.rb +62 -0
- data/lib/sixth_sense/runners/checked_coverage_runner.rb +130 -0
- data/lib/sixth_sense/runners/checked_coverage_trace.rb +110 -0
- data/lib/sixth_sense/runners/coverage_runner.rb +220 -0
- data/lib/sixth_sense/runners/coverage_snapshot.rb +64 -0
- data/lib/sixth_sense/runners/minitest_checked_coverage_probe.rb +42 -0
- data/lib/sixth_sense/runners/minitest_coverage_probe.rb +49 -0
- data/lib/sixth_sense/runners/rspec_checked_coverage_probe.rb +26 -0
- data/lib/sixth_sense/runners/rspec_coverage_probe.rb +98 -0
- data/lib/sixth_sense/runners/test_unit_checked_coverage_probe.rb +43 -0
- data/lib/sixth_sense/runners/test_unit_coverage_probe.rb +51 -0
- data/lib/sixth_sense/scoring/aggregator.rb +117 -0
- data/lib/sixth_sense/source_location.rb +19 -0
- data/lib/sixth_sense/version.rb +5 -0
- data/lib/sixth_sense.rb +74 -0
- metadata +113 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analyzer"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Analyzers
|
|
7
|
+
class QualityFlakiness < Analyzer
|
|
8
|
+
analyzer_id "quality/flakiness"
|
|
9
|
+
axis :quality
|
|
10
|
+
level 0
|
|
11
|
+
|
|
12
|
+
reference authors: "Luo, Q.; Hariri, F.; Eloussi, L.; Marinov, D.",
|
|
13
|
+
title: "An Empirical Analysis of Flaky Tests",
|
|
14
|
+
venue: "FSE",
|
|
15
|
+
year: 2014,
|
|
16
|
+
doi: "10.1145/2635868.2635920"
|
|
17
|
+
|
|
18
|
+
# Paper basis: Luo et al. 2014 (doi:10.1145/2635868.2635920) categorizes
|
|
19
|
+
# flaky-test causes; this analyzer flags static triggers from those classes.
|
|
20
|
+
def analyze(test_file, _context)
|
|
21
|
+
findings = test_file.test_cases.filter_map { |test_case| flaky_finding(test_case) }
|
|
22
|
+
score = [100.0 - (20.0 * findings.length), 0.0].max
|
|
23
|
+
|
|
24
|
+
result(score: score.round(2), findings: findings, confidence: :high)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def flaky_finding(test_case)
|
|
30
|
+
rule = rule_for(test_case.body)
|
|
31
|
+
return unless rule
|
|
32
|
+
|
|
33
|
+
finding(
|
|
34
|
+
rule_id: rule,
|
|
35
|
+
severity: :warning,
|
|
36
|
+
location: test_case.location,
|
|
37
|
+
message: "Example contains a common flaky-test trigger: #{rule}.",
|
|
38
|
+
suggestion: "Replace timing, wall-clock, or global-order dependency with deterministic test control."
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Paper basis: Luo et al. 2014 identifies sleep, wall-clock time, and
|
|
43
|
+
# order/randomness dependencies as common flaky-test causes.
|
|
44
|
+
def rule_for(body)
|
|
45
|
+
return "sleepy_test" if body.match?(/\bsleep(?:\s|\()/)
|
|
46
|
+
return "wall_clock_time" if body.match?(/\b(Time\.now|Date\.today|DateTime\.now)\b/)
|
|
47
|
+
return "order_dependency" if body.match?(/\b(RSpec\.configuration\.order|config\.order|rand\()/)
|
|
48
|
+
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analyzer"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Analyzers
|
|
7
|
+
class QualityTestSmells < Analyzer
|
|
8
|
+
analyzer_id "quality/test_smells"
|
|
9
|
+
axis :quality
|
|
10
|
+
level 0
|
|
11
|
+
|
|
12
|
+
reference authors: "van Deursen, A.; Moonen, L.; van den Bergh, A.; Kok, G.",
|
|
13
|
+
title: "Refactoring Test Code",
|
|
14
|
+
venue: "XP",
|
|
15
|
+
year: 2001
|
|
16
|
+
reference authors: "Meszaros, G.",
|
|
17
|
+
title: "xUnit Test Patterns: Refactoring Test Code",
|
|
18
|
+
venue: "Addison-Wesley",
|
|
19
|
+
year: 2007
|
|
20
|
+
reference authors: "Peruma, A. et al.",
|
|
21
|
+
title: "tsDetect: An Open Source Test Smells Detection Tool",
|
|
22
|
+
venue: "ESEC/FSE",
|
|
23
|
+
year: 2020,
|
|
24
|
+
doi: "10.1145/3368089.3417921"
|
|
25
|
+
reference authors: "Luo, Q.; Hariri, F.; Eloussi, L.; Marinov, D.",
|
|
26
|
+
title: "An Empirical Analysis of Flaky Tests",
|
|
27
|
+
venue: "FSE",
|
|
28
|
+
year: 2014,
|
|
29
|
+
doi: "10.1145/2635868.2635920"
|
|
30
|
+
|
|
31
|
+
WEIGHTS = {
|
|
32
|
+
assertion_roulette: 3,
|
|
33
|
+
eager_test: 3,
|
|
34
|
+
mystery_guest: 3,
|
|
35
|
+
sleepy_test: 3,
|
|
36
|
+
general_fixture: 2,
|
|
37
|
+
conditional_logic: 2,
|
|
38
|
+
sensitive_equality: 2,
|
|
39
|
+
empty_or_pending: 2,
|
|
40
|
+
duplicate_assert: 1,
|
|
41
|
+
magic_number: 1
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# Paper/book basis: van Deursen et al. 2001, Meszaros 2007, and
|
|
45
|
+
# tsDetect/Peruma et al. 2020 (doi:10.1145/3368089.3417921)
|
|
46
|
+
# define the test-smell families aggregated here.
|
|
47
|
+
def analyze(test_file, context)
|
|
48
|
+
findings = test_file.test_cases.flat_map { |test_case| findings_for(test_case, context) }
|
|
49
|
+
findings.reject! { |finding| disabled?(test_file, finding) }
|
|
50
|
+
score = quality_score(test_file, findings, context)
|
|
51
|
+
|
|
52
|
+
result(score: score, findings: findings, confidence: :high)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def findings_for(test_case, context)
|
|
58
|
+
[
|
|
59
|
+
assertion_roulette(test_case, context),
|
|
60
|
+
eager_test(test_case),
|
|
61
|
+
mystery_guest(test_case),
|
|
62
|
+
sensitive_equality(test_case),
|
|
63
|
+
conditional_logic(test_case),
|
|
64
|
+
sleepy_test(test_case),
|
|
65
|
+
empty_or_pending(test_case),
|
|
66
|
+
general_fixture(test_case),
|
|
67
|
+
duplicate_assert(test_case),
|
|
68
|
+
magic_number(test_case)
|
|
69
|
+
].flatten.compact
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reference basis: van Deursen et al. 2001 and Meszaros 2007 describe
|
|
73
|
+
# assertion roulette as multiple unlabeled assertions obscuring failures.
|
|
74
|
+
def assertion_roulette(test_case, context)
|
|
75
|
+
threshold = context.config_fetch(:quality, :assertion_roulette_threshold, default: 3)
|
|
76
|
+
threshold += 2 if test_case.metadata[:aggregate_failures] || test_case.body.include?("aggregate_failures")
|
|
77
|
+
unlabelled = test_case.assertions.count { |assertion| assertion.message.to_s.empty? }
|
|
78
|
+
return if unlabelled < threshold
|
|
79
|
+
|
|
80
|
+
finding(
|
|
81
|
+
rule_id: "assertion_roulette",
|
|
82
|
+
severity: :warning,
|
|
83
|
+
location: test_case.location,
|
|
84
|
+
message: "#{unlabelled} expectations share one example without explanatory messages.",
|
|
85
|
+
suggestion: "Split the example or add expectation messages so failures identify intent."
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Reference basis: van Deursen et al. 2001 and tsDetect/Peruma et al. 2020
|
|
90
|
+
# identify eager tests as examples exercising too many production methods.
|
|
91
|
+
def eager_test(test_case)
|
|
92
|
+
method_calls = test_case.body.scan(/\b([a-z_]\w*)\.([a-z_]\w*[!?=]?)\b/).filter_map do |receiver, method_name|
|
|
93
|
+
next if %w[expect is_expected to not_to to_not should should_not].include?(receiver)
|
|
94
|
+
next if %w[to not_to to_not should should_not eq eql equal include match be].include?(method_name)
|
|
95
|
+
|
|
96
|
+
"#{receiver}.#{method_name}"
|
|
97
|
+
end.uniq
|
|
98
|
+
return if method_calls.length < 3
|
|
99
|
+
|
|
100
|
+
finding(
|
|
101
|
+
rule_id: "eager_test",
|
|
102
|
+
severity: :warning,
|
|
103
|
+
location: test_case.location,
|
|
104
|
+
message: "Example exercises #{method_calls.length} distinct public calls.",
|
|
105
|
+
suggestion: "Keep one behavioral reason to fail per example where practical."
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Reference basis: van Deursen et al. 2001 and Meszaros 2007 define
|
|
110
|
+
# mystery guests as hidden external fixture/file dependencies.
|
|
111
|
+
def mystery_guest(test_case)
|
|
112
|
+
return unless test_case.body.match?(/\b(File\.read|YAML\.load_file|fixture_file|fixtures\/|fixtures_path)\b/)
|
|
113
|
+
|
|
114
|
+
finding(
|
|
115
|
+
rule_id: "mystery_guest",
|
|
116
|
+
severity: :warning,
|
|
117
|
+
location: test_case.location,
|
|
118
|
+
message: "Example depends directly on external fixture or file content.",
|
|
119
|
+
suggestion: "Route fixture setup through named factories or helpers."
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Reference basis: tsDetect/Peruma et al. 2020 includes fragile equality
|
|
124
|
+
# checks over stringified representations among test-smell detectors.
|
|
125
|
+
def sensitive_equality(test_case)
|
|
126
|
+
return unless test_case.body.match?(/(to_s|inspect)\)?\.to\s+(eq|eql|match)|expect\([^)]*\.(to_s|inspect)\)/)
|
|
127
|
+
|
|
128
|
+
finding(
|
|
129
|
+
rule_id: "sensitive_equality",
|
|
130
|
+
severity: :warning,
|
|
131
|
+
location: test_case.location,
|
|
132
|
+
message: "Example compares stringified object representation.",
|
|
133
|
+
suggestion: "Assert on stable domain attributes instead of formatting."
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Reference basis: Meszaros 2007 and tsDetect/Peruma et al. 2020 treat
|
|
138
|
+
# control flow in tests as a maintainability smell.
|
|
139
|
+
def conditional_logic(test_case)
|
|
140
|
+
return unless test_case.body.match?(/\b(if|case|unless|while|until|for)\b|\.each\s+do\b/)
|
|
141
|
+
|
|
142
|
+
finding(
|
|
143
|
+
rule_id: "conditional_logic",
|
|
144
|
+
severity: :warning,
|
|
145
|
+
location: test_case.location,
|
|
146
|
+
message: "Example contains control flow.",
|
|
147
|
+
suggestion: "Move branching setup into separate examples or helpers."
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Paper basis: Luo et al. 2014 (doi:10.1145/2635868.2635920) reports
|
|
152
|
+
# timing and asynchronous waits as common flaky-test triggers.
|
|
153
|
+
def sleepy_test(test_case)
|
|
154
|
+
return unless test_case.body.match?(/\bsleep(?:\s|\()|\bTime\.now\b|\bDate\.today\b/)
|
|
155
|
+
|
|
156
|
+
finding(
|
|
157
|
+
rule_id: "sleepy_test",
|
|
158
|
+
severity: :warning,
|
|
159
|
+
location: test_case.location,
|
|
160
|
+
message: "Example uses sleep or wall-clock time directly.",
|
|
161
|
+
suggestion: "Use deterministic waiting or time-freezing support."
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Reference basis: van Deursen et al. 2001 and Meszaros 2007 describe
|
|
166
|
+
# general fixtures as broad setup not used by each individual test.
|
|
167
|
+
def general_fixture(test_case)
|
|
168
|
+
fixtures = Array(test_case.metadata[:fixtures])
|
|
169
|
+
return if fixtures.empty?
|
|
170
|
+
|
|
171
|
+
unused = fixtures.filter_map do |fixture|
|
|
172
|
+
names = [fixture[:name], *fixture[:ivars]].compact.reject(&:empty?)
|
|
173
|
+
next if names.empty?
|
|
174
|
+
next if names.any? { |name| test_case.body.include?(name.to_s) }
|
|
175
|
+
|
|
176
|
+
fixture
|
|
177
|
+
end
|
|
178
|
+
return if unused.empty?
|
|
179
|
+
|
|
180
|
+
finding(
|
|
181
|
+
rule_id: "general_fixture",
|
|
182
|
+
severity: :warning,
|
|
183
|
+
location: test_case.location,
|
|
184
|
+
message: "Example does not use #{unused.length} fixture setup item(s).",
|
|
185
|
+
suggestion: "Move unused setup into examples that need it or narrower contexts."
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Reference basis: tsDetect/Peruma et al. 2020 includes empty and skipped
|
|
190
|
+
# tests among executable-test quality smells.
|
|
191
|
+
def empty_or_pending(test_case)
|
|
192
|
+
return unless test_case.metadata[:pending] || test_case.body.strip.empty? || test_case.body.match?(/\b(pending|skip)\b/)
|
|
193
|
+
|
|
194
|
+
finding(
|
|
195
|
+
rule_id: "empty_or_pending",
|
|
196
|
+
severity: :warning,
|
|
197
|
+
location: test_case.location,
|
|
198
|
+
message: "Example is empty, pending, or skipped.",
|
|
199
|
+
suggestion: "Remove it or turn it into an executable expectation."
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Reference basis: tsDetect/Peruma et al. 2020 motivates duplicate
|
|
204
|
+
# assertion detection as a low-severity maintainability smell.
|
|
205
|
+
def duplicate_assert(test_case)
|
|
206
|
+
duplicates = test_case.assertions.group_by(&:signature).select { |_signature, assertions| assertions.length > 1 }
|
|
207
|
+
return if duplicates.empty?
|
|
208
|
+
|
|
209
|
+
finding(
|
|
210
|
+
rule_id: "duplicate_assert",
|
|
211
|
+
severity: :info,
|
|
212
|
+
location: test_case.location,
|
|
213
|
+
message: "Example repeats #{duplicates.length} assertion signature(s).",
|
|
214
|
+
suggestion: "Delete duplicate expectations unless repetition documents separate behavior."
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Reference basis: Meszaros 2007 and tsDetect/Peruma et al. 2020 treat
|
|
219
|
+
# unexplained expected literals as a context-dependent test smell.
|
|
220
|
+
def magic_number(test_case)
|
|
221
|
+
literal_expectations = test_case.body.scan(/\b(?:eq|eql|be|be_within)\(?\s*(-?\d+(?:\.\d+)?)\b/)
|
|
222
|
+
return if literal_expectations.length < 3
|
|
223
|
+
|
|
224
|
+
finding(
|
|
225
|
+
rule_id: "magic_number",
|
|
226
|
+
severity: :info,
|
|
227
|
+
location: test_case.location,
|
|
228
|
+
message: "Example uses #{literal_expectations.length} numeric expected values.",
|
|
229
|
+
suggestion: "Name important constants or derive expected values from domain terms."
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Paper basis: Bavota et al. 2015 (doi:10.1007/s10664-014-9313-0) and
|
|
234
|
+
# Spadini et al. 2018 (doi:10.1109/ICSME.2018.00010) motivate weighted
|
|
235
|
+
# smell impact; this score keeps those weights tiered rather than learned.
|
|
236
|
+
def quality_score(test_file, findings, context)
|
|
237
|
+
cap = context.config_fetch(:quality, :smell_cap, default: 5)
|
|
238
|
+
penalty = findings.group_by { |item| item.rule_id.to_sym }.sum do |rule_id, rule_findings|
|
|
239
|
+
WEIGHTS.fetch(rule_id, 1) * [rule_findings.length, cap].min
|
|
240
|
+
end
|
|
241
|
+
loc_norm = [test_file.executable_loc / 100.0, 1.0].max
|
|
242
|
+
quality_raw = 100.0 - (12.0 * penalty / loc_norm)
|
|
243
|
+
[[quality_raw, 0.0].max, 100.0].min.round(2)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def disabled?(test_file, finding)
|
|
247
|
+
source = File.file?(test_file.path) ? File.read(test_file.path) : ""
|
|
248
|
+
directives = source.scan(/#\s*sixth_sense:disable\s+([A-Za-z0-9_\/,-]+)/).flatten.flat_map { |item| item.split(",") }
|
|
249
|
+
directives.include?(finding.rule_id) || directives.include?(finding.analyzer_id) || directives.include?("all")
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../analyzer"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Analyzers
|
|
7
|
+
class RedundancyClone < Analyzer
|
|
8
|
+
analyzer_id "redundancy/clone"
|
|
9
|
+
axis :redundancy
|
|
10
|
+
level 0
|
|
11
|
+
|
|
12
|
+
reference authors: "Roy, C. K.; Cordy, J. R.",
|
|
13
|
+
title: "A Survey on Software Clone Detection Research",
|
|
14
|
+
venue: "Queen's University Technical Report",
|
|
15
|
+
year: 2007
|
|
16
|
+
|
|
17
|
+
# Paper basis: Roy/Cordy 2007 surveys Type-1/2 clone detection; this
|
|
18
|
+
# groups normalized test bodies as a low-confidence clone signal.
|
|
19
|
+
def analyze(test_file, _context)
|
|
20
|
+
groups = test_file.test_cases.group_by { |test_case| normalize_body(test_case.body) }
|
|
21
|
+
clone_groups = groups.reject { |normalized, cases| normalized.empty? || cases.length < 2 }
|
|
22
|
+
duplicate_count = clone_groups.values.sum { |cases| cases.length - 1 }
|
|
23
|
+
score = 100.0 * (1.0 - duplicate_count.to_f / [test_file.test_cases.length, 1].max)
|
|
24
|
+
findings = clone_groups.values.flat_map do |cases|
|
|
25
|
+
cases.drop(1).map do |test_case|
|
|
26
|
+
finding(
|
|
27
|
+
rule_id: "test_clone",
|
|
28
|
+
severity: :info,
|
|
29
|
+
location: test_case.location,
|
|
30
|
+
message: "Example body is a Type-1/2 clone of another example in this file.",
|
|
31
|
+
suggestion: "Extract setup or merge examples only if the behavioral intent is identical."
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
result(score: score.round(2), findings: findings, confidence: :low)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Paper basis: Roy/Cordy 2007 Type-2 clones allow renamed literals/tokens,
|
|
42
|
+
# so literals are normalized before duplicate grouping.
|
|
43
|
+
def normalize_body(body)
|
|
44
|
+
body.to_s
|
|
45
|
+
.gsub(/#.*$/, "")
|
|
46
|
+
.gsub(/(["']).*?\1/, "STR")
|
|
47
|
+
.gsub(/:\w+/, ":SYM")
|
|
48
|
+
.gsub(/\b-?\d+(?:\.\d+)?\b/, "NUM")
|
|
49
|
+
.gsub(/\s+/, " ")
|
|
50
|
+
.strip
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "redundancy_requirement"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Analyzers
|
|
7
|
+
class RedundancyCoverage < Analyzer
|
|
8
|
+
include RedundancyRequirement
|
|
9
|
+
|
|
10
|
+
analyzer_id "redundancy/coverage_based"
|
|
11
|
+
axis :redundancy
|
|
12
|
+
level 1
|
|
13
|
+
|
|
14
|
+
reference authors: "Harrold, M. J.; Gupta, R.; Soffa, M. L.",
|
|
15
|
+
title: "A Methodology for Controlling the Size of a Test Suite",
|
|
16
|
+
venue: "ACM TOSEM",
|
|
17
|
+
year: 1993,
|
|
18
|
+
doi: "10.1145/152388.152391"
|
|
19
|
+
reference authors: "Rothermel, G.; Harrold, M. J.; Ostrin, J.; Hong, C.",
|
|
20
|
+
title: "An Empirical Study of the Effects of Minimization on the Fault Detection Capabilities of Test Suites",
|
|
21
|
+
venue: "ICSM",
|
|
22
|
+
year: 1998,
|
|
23
|
+
doi: "10.1109/ICSM.1998.738487"
|
|
24
|
+
|
|
25
|
+
# Paper basis: Harrold/Gupta/Soffa 1993 (doi:10.1145/152388.152391)
|
|
26
|
+
# models minimization through requirement coverage; Rothermel et al. 1998
|
|
27
|
+
# (doi:10.1109/ICSM.1998.738487) motivates conservative reporting.
|
|
28
|
+
def analyze(test_file, context)
|
|
29
|
+
coverage = context.coverage_map_for(test_file)
|
|
30
|
+
return unmeasured(confidence: :low) unless coverage
|
|
31
|
+
|
|
32
|
+
requirements = test_file.test_cases.to_h do |test_case|
|
|
33
|
+
[test_case.id, coverage.branch_requirements_for(test_case.id)]
|
|
34
|
+
end
|
|
35
|
+
analyze_requirements(test_file, requirements, confidence: :medium)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "redundancy_requirement"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Analyzers
|
|
7
|
+
class RedundancyMutation < Analyzer
|
|
8
|
+
include RedundancyRequirement
|
|
9
|
+
|
|
10
|
+
analyzer_id "redundancy/mutation_based"
|
|
11
|
+
axis :redundancy
|
|
12
|
+
level 2
|
|
13
|
+
|
|
14
|
+
reference authors: "Koochakzadeh, N.; Garousi, V.",
|
|
15
|
+
title: "A Tester-Assisted Methodology for Test Redundancy Detection",
|
|
16
|
+
venue: "Advances in Software Engineering",
|
|
17
|
+
year: 2010,
|
|
18
|
+
doi: "10.1155/2010/932686"
|
|
19
|
+
reference authors: "Yoo, S.; Harman, M.",
|
|
20
|
+
title: "Regression Testing Minimization, Selection and Prioritization: A Survey",
|
|
21
|
+
venue: "STVR",
|
|
22
|
+
year: 2012,
|
|
23
|
+
doi: "10.1002/stvr.430"
|
|
24
|
+
|
|
25
|
+
# Paper basis: Koochakzadeh/Garousi 2010 (doi:10.1155/2010/932686)
|
|
26
|
+
# uses mutation kill sets for redundancy; Yoo/Harman 2012
|
|
27
|
+
# (doi:10.1002/stvr.430) surveys minimization/selection tradeoffs.
|
|
28
|
+
def analyze(test_file, context)
|
|
29
|
+
matrix = context.kill_matrix_for(test_file)
|
|
30
|
+
return unmeasured(confidence: :low) unless matrix&.matrix?
|
|
31
|
+
|
|
32
|
+
requirements = test_file.test_cases.to_h do |test_case|
|
|
33
|
+
[test_case.id, matrix.kill_requirements_for(test_case.id)]
|
|
34
|
+
end
|
|
35
|
+
analyze_requirements(test_file, requirements, confidence: :high)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
require_relative "../analyzer"
|
|
6
|
+
|
|
7
|
+
module SixthSense
|
|
8
|
+
module Analyzers
|
|
9
|
+
module RedundancyRequirement
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Paper basis: Harrold/Gupta/Soffa 1993 reduces suite size by retaining
|
|
13
|
+
# representatives that cover requirements; Rothermel et al. 1998 motivates
|
|
14
|
+
# preserving oracle-distinct tests instead of blindly deleting them.
|
|
15
|
+
def analyze_requirements(test_file, requirements_by_id, confidence:)
|
|
16
|
+
return result(score: 100.0, findings: [], confidence: confidence) if test_file.test_cases.empty?
|
|
17
|
+
|
|
18
|
+
selected_ids = greedy_representatives(test_file, requirements_by_id)
|
|
19
|
+
redundant = test_file.test_cases.reject { |test_case| selected_ids.include?(test_case.id) }
|
|
20
|
+
penalties = redundant.to_h do |test_case|
|
|
21
|
+
[test_case, oracle_penalty(test_case, test_file, selected_ids)]
|
|
22
|
+
end
|
|
23
|
+
score = 100.0 * (1.0 - penalties.values.sum / [test_file.test_cases.length, 1].max)
|
|
24
|
+
findings = penalties.map do |test_case, penalty|
|
|
25
|
+
finding(
|
|
26
|
+
rule_id: "redundant_candidate",
|
|
27
|
+
severity: :warning,
|
|
28
|
+
location: test_case.location,
|
|
29
|
+
message: "Example is covered by the representative requirement set.",
|
|
30
|
+
suggestion: penalty >= 1.0 ? "Review for consolidation." : "Coverage overlaps, but assertions differ; review manually."
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result(score: [[score, 0.0].max, 100.0].min.round(2), findings: findings, confidence: confidence)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Paper basis: Harrold/Gupta/Soffa 1993; greedy set-cover approximation
|
|
38
|
+
# selects tests that cover the most still-uncovered requirements.
|
|
39
|
+
def greedy_representatives(test_file, requirements_by_id)
|
|
40
|
+
uncovered = requirements_by_id.values.reduce(Set.new, :|)
|
|
41
|
+
selected = Set.new
|
|
42
|
+
|
|
43
|
+
until uncovered.empty?
|
|
44
|
+
candidate = test_file.test_cases.max_by do |test_case|
|
|
45
|
+
new_coverage = requirements_by_id.fetch(test_case.id, Set.new) & uncovered
|
|
46
|
+
[new_coverage.length, test_case.assertions.length, -(test_case.location&.line || 0)]
|
|
47
|
+
end
|
|
48
|
+
break unless candidate
|
|
49
|
+
|
|
50
|
+
covered_now = requirements_by_id.fetch(candidate.id, Set.new) & uncovered
|
|
51
|
+
break if covered_now.empty?
|
|
52
|
+
|
|
53
|
+
selected << candidate.id
|
|
54
|
+
uncovered -= covered_now
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
selected
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Paper basis: Rothermel et al. 1998; overlap is penalized less when the
|
|
61
|
+
# assertion/oracle signature differs, preserving possible fault detection.
|
|
62
|
+
def oracle_penalty(test_case, test_file, selected_ids)
|
|
63
|
+
signature_set = test_case.assertions.map(&:signature).to_set
|
|
64
|
+
selected_signature_sets = test_file.test_cases.select { |item| selected_ids.include?(item.id) }
|
|
65
|
+
.map { |item| item.assertions.map(&:signature).to_set }
|
|
66
|
+
selected_signature_sets.include?(signature_set) ? 1.0 : 0.5
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
class ChangedFiles
|
|
7
|
+
def self.test_files(diff_ref)
|
|
8
|
+
new(diff_ref).test_files
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(diff_ref)
|
|
12
|
+
@diff_ref = diff_ref
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_files
|
|
16
|
+
names = changed_names
|
|
17
|
+
specs = names.select { |path| path.end_with?("_spec.rb") && File.file?(path) }
|
|
18
|
+
inferred_specs = names.flat_map { |path| infer_spec_paths(path) }
|
|
19
|
+
(specs + inferred_specs).uniq.sort
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :diff_ref
|
|
25
|
+
|
|
26
|
+
def changed_names
|
|
27
|
+
output, status = Open3.capture2("git", "diff", "--name-only", diff_ref)
|
|
28
|
+
return [] unless status.success?
|
|
29
|
+
|
|
30
|
+
output.lines.map(&:strip).reject(&:empty?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def infer_spec_paths(path)
|
|
34
|
+
return [] unless path.start_with?("lib/", "app/")
|
|
35
|
+
return [] unless path.end_with?(".rb")
|
|
36
|
+
|
|
37
|
+
relative = path.sub(%r{\A(?:lib|app)/}, "").sub(/\.rb\z/, "_spec.rb")
|
|
38
|
+
candidates = [File.join("spec", relative)]
|
|
39
|
+
if path.start_with?("app/")
|
|
40
|
+
candidates << File.join("spec", path.sub(%r{\Aapp/}, "").sub(/\.rb\z/, "_spec.rb"))
|
|
41
|
+
end
|
|
42
|
+
candidates.select { |candidate| File.file?(candidate) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|