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,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "checked_coverage_trace"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Runners
|
|
7
|
+
class MinitestCheckedCoverageProbe
|
|
8
|
+
class << self
|
|
9
|
+
def run(test_path, test_name)
|
|
10
|
+
status = CheckedCoverageTrace.capture do
|
|
11
|
+
require File.expand_path(test_path)
|
|
12
|
+
passed = run_test(test_name)
|
|
13
|
+
clear_minitest_runnables
|
|
14
|
+
passed ? 0 : 1
|
|
15
|
+
end
|
|
16
|
+
exit(status)
|
|
17
|
+
rescue LoadError => error
|
|
18
|
+
warn error.message
|
|
19
|
+
exit(127)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def run_test(test_name)
|
|
25
|
+
Minitest.seed ||= 0
|
|
26
|
+
klass = minitest_classes.find { |candidate| candidate.runnable_methods.include?(test_name) }
|
|
27
|
+
return false unless klass
|
|
28
|
+
|
|
29
|
+
klass.new(test_name).run.passed?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def minitest_classes
|
|
33
|
+
Minitest::Runnable.runnables.select { |candidate| candidate < Minitest::Test }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear_minitest_runnables
|
|
37
|
+
Minitest::Runnable.runnables.clear
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "coverage_snapshot"
|
|
7
|
+
|
|
8
|
+
module SixthSense
|
|
9
|
+
module Runners
|
|
10
|
+
class MinitestCoverageProbe
|
|
11
|
+
class << self
|
|
12
|
+
def run(test_path, test_name)
|
|
13
|
+
Coverage.start(lines: true, branches: true)
|
|
14
|
+
require File.expand_path(test_path)
|
|
15
|
+
passed = run_test(test_name)
|
|
16
|
+
clear_minitest_runnables
|
|
17
|
+
result = Coverage.result
|
|
18
|
+
write_snapshot(result)
|
|
19
|
+
exit(passed ? 0 : 1)
|
|
20
|
+
rescue LoadError => error
|
|
21
|
+
warn error.message
|
|
22
|
+
exit(127)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run_test(test_name)
|
|
28
|
+
Minitest.seed ||= 0
|
|
29
|
+
klass = minitest_classes.find { |candidate| candidate.runnable_methods.include?(test_name) }
|
|
30
|
+
return false unless klass
|
|
31
|
+
|
|
32
|
+
klass.new(test_name).run.passed?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def minitest_classes
|
|
36
|
+
Minitest::Runnable.runnables.select { |candidate| candidate < Minitest::Test }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear_minitest_runnables
|
|
40
|
+
Minitest::Runnable.runnables.clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_snapshot(result)
|
|
44
|
+
File.write(ENV.fetch("SIXTH_SENSE_COVERAGE_OUTPUT"), JSON.generate(CoverageSnapshot.snapshot_for(result)))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
require_relative "checked_coverage_trace"
|
|
6
|
+
|
|
7
|
+
module SixthSense
|
|
8
|
+
module Runners
|
|
9
|
+
class RSpecCheckedCoverageProbe
|
|
10
|
+
class << self
|
|
11
|
+
def run(spec_path, line)
|
|
12
|
+
require "rspec/core"
|
|
13
|
+
require "rspec/expectations"
|
|
14
|
+
|
|
15
|
+
status = CheckedCoverageTrace.capture do
|
|
16
|
+
RSpec::Core::Runner.run(["--options", File::NULL, "#{spec_path}:#{line}"], StringIO.new, StringIO.new)
|
|
17
|
+
end
|
|
18
|
+
exit(status)
|
|
19
|
+
rescue LoadError => error
|
|
20
|
+
warn error.message
|
|
21
|
+
exit(127)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
require "json"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module SixthSense
|
|
8
|
+
module Runners
|
|
9
|
+
class RSpecCoverageProbe
|
|
10
|
+
class << self
|
|
11
|
+
def run(spec_path, line)
|
|
12
|
+
require "rspec/core"
|
|
13
|
+
require "rspec/expectations"
|
|
14
|
+
|
|
15
|
+
Coverage.start(lines: true, branches: true)
|
|
16
|
+
status = RSpec::Core::Runner.run(["--options", File::NULL, "#{spec_path}:#{line}"], StringIO.new, StringIO.new)
|
|
17
|
+
result = Coverage.result
|
|
18
|
+
write_snapshot(result)
|
|
19
|
+
exit(status)
|
|
20
|
+
rescue LoadError => error
|
|
21
|
+
warn error.message
|
|
22
|
+
exit(127)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def write_snapshot(result)
|
|
28
|
+
output = ENV.fetch("SIXTH_SENSE_COVERAGE_OUTPUT")
|
|
29
|
+
File.write(output, JSON.generate(snapshot_for(result)))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def snapshot_for(result)
|
|
33
|
+
lines = {}
|
|
34
|
+
executable_lines = {}
|
|
35
|
+
branches = {}
|
|
36
|
+
executable_branches = {}
|
|
37
|
+
result.each do |path, payload|
|
|
38
|
+
if payload.is_a?(Hash)
|
|
39
|
+
lines[path] = covered_lines(payload[:lines] || payload["lines"])
|
|
40
|
+
executable_lines[path] = executable_lines(payload[:lines] || payload["lines"])
|
|
41
|
+
branches[path] = covered_branches(payload[:branches] || payload["branches"])
|
|
42
|
+
executable_branches[path] = executable_branches(payload[:branches] || payload["branches"])
|
|
43
|
+
else
|
|
44
|
+
lines[path] = covered_lines(payload)
|
|
45
|
+
executable_lines[path] = executable_lines(payload)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
{
|
|
49
|
+
lines: lines,
|
|
50
|
+
executable_lines: executable_lines,
|
|
51
|
+
branches: branches,
|
|
52
|
+
executable_branches: executable_branches
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def covered_lines(line_counts)
|
|
57
|
+
Array(line_counts).each_with_index.filter_map do |count, index|
|
|
58
|
+
index + 1 if count.to_i.positive?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def executable_lines(line_counts)
|
|
63
|
+
Array(line_counts).each_with_index.filter_map do |count, index|
|
|
64
|
+
index + 1 unless count.nil?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def covered_branches(branches)
|
|
69
|
+
return [] unless branches.respond_to?(:each)
|
|
70
|
+
|
|
71
|
+
branches.each_with_index.filter_map do |branch, index|
|
|
72
|
+
"#{branch_key(branch)}:#{index}" if branch_executed?(branch)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def executable_branches(branches)
|
|
77
|
+
return [] unless branches.respond_to?(:each)
|
|
78
|
+
|
|
79
|
+
branches.each_with_index.map do |branch, index|
|
|
80
|
+
"#{branch_key(branch)}:#{index}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def branch_key(branch)
|
|
85
|
+
branch.respond_to?(:first) ? branch.first.inspect : branch.inspect
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def branch_executed?(branch)
|
|
89
|
+
values = branch.respond_to?(:last) ? branch.last : branch
|
|
90
|
+
return values.to_i.positive? if values.is_a?(Integer)
|
|
91
|
+
return values.values.any? { |count| count.to_i.positive? } if values.respond_to?(:values)
|
|
92
|
+
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "checked_coverage_trace"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Runners
|
|
7
|
+
class TestUnitCheckedCoverageProbe
|
|
8
|
+
class << self
|
|
9
|
+
def run(test_path, test_name)
|
|
10
|
+
status = CheckedCoverageTrace.capture do
|
|
11
|
+
require "test/unit"
|
|
12
|
+
require "test/unit/testresult"
|
|
13
|
+
require "test/unit/worker-context"
|
|
14
|
+
Test::Unit::AutoRunner.need_auto_run = false
|
|
15
|
+
require File.expand_path(test_path)
|
|
16
|
+
run_test(test_case_classes, test_name) ? 0 : 1
|
|
17
|
+
end
|
|
18
|
+
exit(status)
|
|
19
|
+
rescue LoadError => error
|
|
20
|
+
warn error.message
|
|
21
|
+
exit(127)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def run_test(classes, test_name)
|
|
27
|
+
klass = classes.find { |candidate| candidate.public_instance_methods.map(&:to_s).include?(test_name) }
|
|
28
|
+
return false unless klass
|
|
29
|
+
|
|
30
|
+
result = Test::Unit::TestResult.new
|
|
31
|
+
test = klass.new(test_name)
|
|
32
|
+
test.instance_variable_set(:@passed_assertions, [])
|
|
33
|
+
test.run(result) { |_event, *_args| }
|
|
34
|
+
result.passed?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_case_classes
|
|
38
|
+
ObjectSpace.each_object(Class).select { |klass| klass < Test::Unit::TestCase }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "coverage_snapshot"
|
|
7
|
+
|
|
8
|
+
module SixthSense
|
|
9
|
+
module Runners
|
|
10
|
+
class TestUnitCoverageProbe
|
|
11
|
+
class << self
|
|
12
|
+
def run(test_path, test_name)
|
|
13
|
+
Coverage.start(lines: true, branches: true)
|
|
14
|
+
require "test/unit"
|
|
15
|
+
require "test/unit/testresult"
|
|
16
|
+
require "test/unit/worker-context"
|
|
17
|
+
Test::Unit::AutoRunner.need_auto_run = false
|
|
18
|
+
require File.expand_path(test_path)
|
|
19
|
+
passed = run_test(test_case_classes, test_name)
|
|
20
|
+
result = Coverage.result
|
|
21
|
+
write_snapshot(result)
|
|
22
|
+
exit(passed ? 0 : 1)
|
|
23
|
+
rescue LoadError => error
|
|
24
|
+
warn error.message
|
|
25
|
+
exit(127)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def run_test(classes, test_name)
|
|
31
|
+
klass = classes.find { |candidate| candidate.public_instance_methods.map(&:to_s).include?(test_name) }
|
|
32
|
+
return false unless klass
|
|
33
|
+
|
|
34
|
+
result = Test::Unit::TestResult.new
|
|
35
|
+
test = klass.new(test_name)
|
|
36
|
+
test.instance_variable_set(:@passed_assertions, [])
|
|
37
|
+
test.run(result) { |_event, *_args| }
|
|
38
|
+
result.passed?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_case_classes
|
|
42
|
+
ObjectSpace.each_object(Class).select { |klass| klass < Test::Unit::TestCase }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_snapshot(result)
|
|
46
|
+
File.write(ENV.fetch("SIXTH_SENSE_COVERAGE_OUTPUT"), JSON.generate(CoverageSnapshot.snapshot_for(result)))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../result"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
module Scoring
|
|
7
|
+
class Aggregator
|
|
8
|
+
AXES = %i[adequacy redundancy quality].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(level:)
|
|
11
|
+
@level = level
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze(test_file, context)
|
|
15
|
+
analyzer_results = applicable_analyzers.map { |analyzer_class| analyzer_class.new.analyze(test_file, context) }
|
|
16
|
+
axis_scores = AXES.map { |axis| axis_score(axis, analyzer_results) }
|
|
17
|
+
measured_values = axis_scores.select(&:measured?).map(&:value)
|
|
18
|
+
|
|
19
|
+
Result::FileReport.new(
|
|
20
|
+
test_file: test_file,
|
|
21
|
+
axis_scores: axis_scores,
|
|
22
|
+
composite: measured_values.empty? ? nil : measured_values.min.round(2),
|
|
23
|
+
level: @level,
|
|
24
|
+
warnings: context.warnings
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def applicable_analyzers
|
|
31
|
+
SixthSense.analyzers.select do |analyzer_class|
|
|
32
|
+
analyzer_class.level && analyzer_class.level <= @level
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def axis_score(axis, analyzer_results)
|
|
37
|
+
results = analyzer_results.select { |item| item.axis == axis }
|
|
38
|
+
findings = results.flat_map(&:findings)
|
|
39
|
+
measured = results.select(&:measured?)
|
|
40
|
+
|
|
41
|
+
case axis
|
|
42
|
+
when :adequacy
|
|
43
|
+
adequacy_score(measured, findings)
|
|
44
|
+
when :redundancy
|
|
45
|
+
prioritized_score(axis, measured, findings, %w[redundancy/mutation_based redundancy/coverage_based redundancy/clone])
|
|
46
|
+
when :quality
|
|
47
|
+
minimum_score(axis, measured, findings)
|
|
48
|
+
else
|
|
49
|
+
unmeasured_axis(axis, findings)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Paper basis: Just et al. 2014 and Papadakis et al. 2018 motivate the
|
|
54
|
+
# mutation-heavy weighting; Inozemtseva/Holmes 2014 and Schuler/Zeller
|
|
55
|
+
# 2011 keep coverage and checked coverage as lower-weight signals.
|
|
56
|
+
def adequacy_score(results, findings)
|
|
57
|
+
mutation = results.find { |item| item.analyzer_id == "adequacy/mutation" }
|
|
58
|
+
coverage = results.find { |item| item.analyzer_id == "adequacy/coverage" }
|
|
59
|
+
checked = results.find { |item| item.analyzer_id == "adequacy/checked_coverage" }
|
|
60
|
+
weighted = []
|
|
61
|
+
weighted << [mutation.score, 0.60] if mutation
|
|
62
|
+
weighted << [coverage.score, 0.25] if coverage
|
|
63
|
+
weighted << [checked.score, 0.15] if checked
|
|
64
|
+
return unmeasured_axis(:adequacy, findings) if weighted.empty?
|
|
65
|
+
|
|
66
|
+
total_weight = weighted.sum(&:last)
|
|
67
|
+
value = weighted.sum { |score, weight| score * (weight / total_weight) }
|
|
68
|
+
confidence = mutation ? :high : :medium
|
|
69
|
+
Result::AxisScore.new(
|
|
70
|
+
axis: :adequacy,
|
|
71
|
+
value: value.round(2),
|
|
72
|
+
confidence: confidence,
|
|
73
|
+
findings: findings,
|
|
74
|
+
measured: true
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Paper basis: Koochakzadeh/Garousi 2010 makes mutation requirements the
|
|
79
|
+
# more precise redundancy signal, with coverage minimization as fallback.
|
|
80
|
+
def prioritized_score(axis, results, findings, analyzer_ids)
|
|
81
|
+
selected = analyzer_ids.filter_map { |id| results.find { |item| item.analyzer_id == id } }.first
|
|
82
|
+
return unmeasured_axis(axis, findings) unless selected
|
|
83
|
+
|
|
84
|
+
Result::AxisScore.new(
|
|
85
|
+
axis: axis,
|
|
86
|
+
value: selected.score.round(2),
|
|
87
|
+
confidence: selected.confidence,
|
|
88
|
+
findings: selected.findings,
|
|
89
|
+
measured: true
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def minimum_score(axis, results, findings)
|
|
94
|
+
return unmeasured_axis(axis, findings) if results.empty?
|
|
95
|
+
|
|
96
|
+
selected = results.min_by(&:score)
|
|
97
|
+
Result::AxisScore.new(
|
|
98
|
+
axis: axis,
|
|
99
|
+
value: selected.score.round(2),
|
|
100
|
+
confidence: selected.confidence,
|
|
101
|
+
findings: findings,
|
|
102
|
+
measured: true
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def unmeasured_axis(axis, findings)
|
|
107
|
+
Result::AxisScore.new(
|
|
108
|
+
axis: axis,
|
|
109
|
+
value: nil,
|
|
110
|
+
confidence: :low,
|
|
111
|
+
findings: findings,
|
|
112
|
+
measured: false
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SixthSense
|
|
4
|
+
SourceLocation = Struct.new(:path, :line, :column, keyword_init: true) do
|
|
5
|
+
def to_h
|
|
6
|
+
{
|
|
7
|
+
path: path,
|
|
8
|
+
line: line,
|
|
9
|
+
column: column
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
return path.to_s unless line
|
|
15
|
+
|
|
16
|
+
[path, line, column].compact.join(":")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/sixth_sense.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sixth_sense/version"
|
|
4
|
+
|
|
5
|
+
module SixthSense
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
class Plugin
|
|
9
|
+
def adapter(name, adapter_class)
|
|
10
|
+
FrameworkAdapter.register(name, adapter_class)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def analyzer(analyzer_class)
|
|
14
|
+
SixthSense.register_analyzer(analyzer_class)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def analyzers
|
|
20
|
+
@analyzers ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def register_analyzer(analyzer_class)
|
|
24
|
+
return if analyzers.include?(analyzer_class)
|
|
25
|
+
|
|
26
|
+
analyzers << analyzer_class
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def plugin
|
|
30
|
+
registry = Plugin.new
|
|
31
|
+
yield registry if block_given?
|
|
32
|
+
registry
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
require_relative "sixth_sense/source_location"
|
|
38
|
+
require_relative "sixth_sense/model"
|
|
39
|
+
require_relative "sixth_sense/result"
|
|
40
|
+
require_relative "sixth_sense/framework_adapter"
|
|
41
|
+
require_relative "sixth_sense/analyzer"
|
|
42
|
+
require_relative "sixth_sense/analysis_context"
|
|
43
|
+
require_relative "sixth_sense/adapters/rspec"
|
|
44
|
+
require_relative "sixth_sense/adapters/test_unit"
|
|
45
|
+
require_relative "sixth_sense/adapters/minitest"
|
|
46
|
+
require_relative "sixth_sense/analyzers/quality_test_smells"
|
|
47
|
+
require_relative "sixth_sense/analyzers/quality_assertion_density"
|
|
48
|
+
require_relative "sixth_sense/analyzers/quality_flakiness"
|
|
49
|
+
require_relative "sixth_sense/analyzers/adequacy_coverage"
|
|
50
|
+
require_relative "sixth_sense/analyzers/adequacy_mutation"
|
|
51
|
+
require_relative "sixth_sense/analyzers/adequacy_checked_coverage"
|
|
52
|
+
require_relative "sixth_sense/analyzers/redundancy_coverage"
|
|
53
|
+
require_relative "sixth_sense/analyzers/redundancy_mutation"
|
|
54
|
+
require_relative "sixth_sense/analyzers/redundancy_clone"
|
|
55
|
+
require_relative "sixth_sense/scoring/aggregator"
|
|
56
|
+
require_relative "sixth_sense/reporters/console"
|
|
57
|
+
require_relative "sixth_sense/reporters/json"
|
|
58
|
+
require_relative "sixth_sense/reporters/markdown"
|
|
59
|
+
require_relative "sixth_sense/reporters/sarif"
|
|
60
|
+
require_relative "sixth_sense/reporters/html"
|
|
61
|
+
require_relative "sixth_sense/config"
|
|
62
|
+
require_relative "sixth_sense/analysis_runner"
|
|
63
|
+
require_relative "sixth_sense/mutation_engine"
|
|
64
|
+
require_relative "sixth_sense/mutation_cache"
|
|
65
|
+
require_relative "sixth_sense/mutation_matrix_mutant_generator"
|
|
66
|
+
require_relative "sixth_sense/mutation_matrix_producer"
|
|
67
|
+
require_relative "sixth_sense/mutation_score_cache_writer"
|
|
68
|
+
require_relative "sixth_sense/runners/coverage_runner"
|
|
69
|
+
require_relative "sixth_sense/runners/minitest_coverage_probe"
|
|
70
|
+
require_relative "sixth_sense/runners/test_unit_coverage_probe"
|
|
71
|
+
require_relative "sixth_sense/runners/minitest_checked_coverage_probe"
|
|
72
|
+
require_relative "sixth_sense/runners/test_unit_checked_coverage_probe"
|
|
73
|
+
require_relative "sixth_sense/runners/checked_coverage_runner"
|
|
74
|
+
require_relative "sixth_sense/runners/checked_coverage_estimator"
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sixth_sense
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.24'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.24'
|
|
26
|
+
description: Evaluates adequacy, redundancy, and maintainability of Ruby test suites
|
|
27
|
+
using coverage, checked coverage, mutation data, and test-smell analysis.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- sixth_sense
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- exe/sixth_sense
|
|
38
|
+
- lib/sixth_sense.rb
|
|
39
|
+
- lib/sixth_sense/adapters/minitest.rb
|
|
40
|
+
- lib/sixth_sense/adapters/rspec.rb
|
|
41
|
+
- lib/sixth_sense/adapters/test_unit.rb
|
|
42
|
+
- lib/sixth_sense/analysis_context.rb
|
|
43
|
+
- lib/sixth_sense/analysis_runner.rb
|
|
44
|
+
- lib/sixth_sense/analyzer.rb
|
|
45
|
+
- lib/sixth_sense/analyzers/adequacy_checked_coverage.rb
|
|
46
|
+
- lib/sixth_sense/analyzers/adequacy_coverage.rb
|
|
47
|
+
- lib/sixth_sense/analyzers/adequacy_mutation.rb
|
|
48
|
+
- lib/sixth_sense/analyzers/quality_assertion_density.rb
|
|
49
|
+
- lib/sixth_sense/analyzers/quality_flakiness.rb
|
|
50
|
+
- lib/sixth_sense/analyzers/quality_test_smells.rb
|
|
51
|
+
- lib/sixth_sense/analyzers/redundancy_clone.rb
|
|
52
|
+
- lib/sixth_sense/analyzers/redundancy_coverage.rb
|
|
53
|
+
- lib/sixth_sense/analyzers/redundancy_mutation.rb
|
|
54
|
+
- lib/sixth_sense/analyzers/redundancy_requirement.rb
|
|
55
|
+
- lib/sixth_sense/changed_files.rb
|
|
56
|
+
- lib/sixth_sense/cli.rb
|
|
57
|
+
- lib/sixth_sense/config.rb
|
|
58
|
+
- lib/sixth_sense/engines/mutant.rb
|
|
59
|
+
- lib/sixth_sense/framework_adapter.rb
|
|
60
|
+
- lib/sixth_sense/guardrail/baseline.rb
|
|
61
|
+
- lib/sixth_sense/guardrail/evaluator.rb
|
|
62
|
+
- lib/sixth_sense/model.rb
|
|
63
|
+
- lib/sixth_sense/mutation_cache.rb
|
|
64
|
+
- lib/sixth_sense/mutation_engine.rb
|
|
65
|
+
- lib/sixth_sense/mutation_matrix_mutant_generator.rb
|
|
66
|
+
- lib/sixth_sense/mutation_matrix_producer.rb
|
|
67
|
+
- lib/sixth_sense/mutation_score_cache_writer.rb
|
|
68
|
+
- lib/sixth_sense/rake_task.rb
|
|
69
|
+
- lib/sixth_sense/reporters/console.rb
|
|
70
|
+
- lib/sixth_sense/reporters/html.rb
|
|
71
|
+
- lib/sixth_sense/reporters/json.rb
|
|
72
|
+
- lib/sixth_sense/reporters/markdown.rb
|
|
73
|
+
- lib/sixth_sense/reporters/sarif.rb
|
|
74
|
+
- lib/sixth_sense/result.rb
|
|
75
|
+
- lib/sixth_sense/runners/checked_coverage_estimator.rb
|
|
76
|
+
- lib/sixth_sense/runners/checked_coverage_runner.rb
|
|
77
|
+
- lib/sixth_sense/runners/checked_coverage_trace.rb
|
|
78
|
+
- lib/sixth_sense/runners/coverage_runner.rb
|
|
79
|
+
- lib/sixth_sense/runners/coverage_snapshot.rb
|
|
80
|
+
- lib/sixth_sense/runners/minitest_checked_coverage_probe.rb
|
|
81
|
+
- lib/sixth_sense/runners/minitest_coverage_probe.rb
|
|
82
|
+
- lib/sixth_sense/runners/rspec_checked_coverage_probe.rb
|
|
83
|
+
- lib/sixth_sense/runners/rspec_coverage_probe.rb
|
|
84
|
+
- lib/sixth_sense/runners/test_unit_checked_coverage_probe.rb
|
|
85
|
+
- lib/sixth_sense/runners/test_unit_coverage_probe.rb
|
|
86
|
+
- lib/sixth_sense/scoring/aggregator.rb
|
|
87
|
+
- lib/sixth_sense/source_location.rb
|
|
88
|
+
- lib/sixth_sense/version.rb
|
|
89
|
+
homepage: https://github.com/ydah/sixth_sense
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata:
|
|
93
|
+
homepage_uri: https://github.com/ydah/sixth_sense
|
|
94
|
+
source_code_uri: https://github.com/ydah/sixth_sense/tree/main
|
|
95
|
+
rubygems_mfa_required: 'true'
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 3.2.0
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 4.0.6
|
|
111
|
+
specification_version: 4
|
|
112
|
+
summary: Evidence-based test suite adequacy analysis for Ruby test files.
|
|
113
|
+
test_files: []
|