simplecov-ai 0.10.4 → 0.10.5
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
- checksums.yaml.gz.sig +0 -0
- data/lib/simplecov-ai/ast_resolver/semantic_node.rb +47 -0
- data/lib/simplecov-ai/ast_resolver.rb +29 -56
- data/lib/simplecov-ai/configuration.rb +19 -6
- data/lib/simplecov-ai/constants.rb +20 -0
- data/lib/simplecov-ai/markdown_builder/branch_enricher.rb +70 -0
- data/lib/simplecov-ai/markdown_builder/bypass_compiler.rb +27 -17
- data/lib/simplecov-ai/markdown_builder/deficit_compiler.rb +55 -76
- data/lib/simplecov-ai/markdown_builder/deficit_formatter.rb +142 -0
- data/lib/simplecov-ai/markdown_builder/deficit_grouper.rb +17 -10
- data/lib/simplecov-ai/markdown_builder/snippet_formatter.rb +23 -12
- data/lib/simplecov-ai/markdown_builder.rb +57 -25
- data/lib/simplecov-ai/version.rb +1 -1
- data/lib/simplecov-ai.rb +9 -5
- data.tar.gz.sig +0 -0
- metadata +6 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72c1e34acc5609236e3551bf3759099948f9dd98ae12b04ee249ea0e03682e79
|
|
4
|
+
data.tar.gz: 84d74a91bc0ac99d9841242aa095a5876316f8493e8a1d9ac8be1aca1146003c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4339ac621867c733eebbc80bcb6b8453839e210f3ce6e9017a276d4342b48c1aa2e9e068f7068b3780e33934edffdcda5afec4ffb670b9a2dee214ea68f5ebd
|
|
7
|
+
data.tar.gz: 1cc89347ca1d591d0f207d81dde82864062743b8cc8b981b71c5f7f2529ae0f06898d4949c9a1f7ed5916b943cb510a4a4528235acb15135d1f8fd116bb25ae8
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class ASTResolver
|
|
8
|
+
# An immutable struct housing bounds, identification metrics, and static bypassing
|
|
9
|
+
# definitions derived from traversing the AST nodes.
|
|
10
|
+
class SemanticNode
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { returns(String) }
|
|
14
|
+
attr_reader :name, :type
|
|
15
|
+
|
|
16
|
+
sig { returns(Integer) }
|
|
17
|
+
attr_reader :start_line, :end_line
|
|
18
|
+
|
|
19
|
+
sig { returns(T::Array[String]) }
|
|
20
|
+
attr_reader :bypass_reasons
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(
|
|
24
|
+
name: String,
|
|
25
|
+
type: String,
|
|
26
|
+
start_line: Integer,
|
|
27
|
+
end_line: Integer,
|
|
28
|
+
bypass_reasons: T::Array[String]
|
|
29
|
+
).void
|
|
30
|
+
end
|
|
31
|
+
def initialize(name:, type:, start_line:, end_line:, bypass_reasons: [])
|
|
32
|
+
@name = name
|
|
33
|
+
@type = type
|
|
34
|
+
@start_line = start_line
|
|
35
|
+
@end_line = end_line
|
|
36
|
+
@bypass_reasons = bypass_reasons
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sig { params(bypass_reason: String).void }
|
|
40
|
+
def add_bypass(bypass_reason)
|
|
41
|
+
@bypass_reasons << bypass_reason
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'parser/current'
|
|
5
|
+
require_relative 'ast_resolver/semantic_node'
|
|
5
6
|
|
|
6
7
|
module SimpleCov
|
|
7
8
|
module Formatter
|
|
@@ -13,42 +14,16 @@ module SimpleCov
|
|
|
13
14
|
class ASTResolver
|
|
14
15
|
extend T::Sig
|
|
15
16
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
sig { returns(T::Array[String]) }
|
|
28
|
-
attr_reader :bypasses
|
|
29
|
-
|
|
30
|
-
sig do
|
|
31
|
-
params(
|
|
32
|
-
name: String,
|
|
33
|
-
type: String,
|
|
34
|
-
start_line: Integer,
|
|
35
|
-
end_line: Integer,
|
|
36
|
-
bypasses: T::Array[String]
|
|
37
|
-
).void
|
|
38
|
-
end
|
|
39
|
-
def initialize(name:, type:, start_line:, end_line:, bypasses: [])
|
|
40
|
-
@name = name
|
|
41
|
-
@type = type
|
|
42
|
-
@start_line = start_line
|
|
43
|
-
@end_line = end_line
|
|
44
|
-
@bypasses = bypasses
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
sig { params(bypass: String).void }
|
|
48
|
-
def add_bypass(bypass)
|
|
49
|
-
@bypasses << bypass
|
|
50
|
-
end
|
|
51
|
-
end
|
|
17
|
+
# Separator used to denote namespace nesting (e.g., Module::Class)
|
|
18
|
+
NAMESPACE_SEPARATOR = T.let('::', String)
|
|
19
|
+
# Separator used to denote instance methods (e.g., Class#method)
|
|
20
|
+
INSTANCE_SEPARATOR = T.let('#', String)
|
|
21
|
+
# Separator used to denote singleton/class methods (e.g., Class.method)
|
|
22
|
+
SINGLETON_SEPARATOR = T.let('.', String)
|
|
23
|
+
# Label applied to nodes representing instance methods
|
|
24
|
+
TYPE_INSTANCE_METHOD = T.let('Instance Method', String)
|
|
25
|
+
# Label applied to nodes representing singleton methods
|
|
26
|
+
TYPE_SINGLETON_METHOD = T.let('Singleton Method', String)
|
|
52
27
|
|
|
53
28
|
# Orchestrates the initial mapping algorithm on a target file to extract structural
|
|
54
29
|
# metadata, circumventing potential syntax violations explicitly.
|
|
@@ -94,23 +69,21 @@ module SimpleCov
|
|
|
94
69
|
|
|
95
70
|
sig { params(nodes: T::Array[SemanticNode], comments: T::Array[Parser::Source::Comment]).void }
|
|
96
71
|
def assign_bypasses(nodes, comments)
|
|
97
|
-
comments.each do |
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assign_bypass(nodes, c, c_text.strip)
|
|
72
|
+
comments.each do |comment|
|
|
73
|
+
comment_text = T.cast(comment.text, String)
|
|
74
|
+
assign_bypass(nodes, comment, comment_text.strip) if comment_text.include?(Constants::NOCOV_DIRECTIVE)
|
|
102
75
|
end
|
|
103
76
|
end
|
|
104
77
|
|
|
105
78
|
private
|
|
106
79
|
|
|
107
|
-
sig { params(nodes: T::Array[SemanticNode], comment: Parser::Source::Comment,
|
|
108
|
-
def assign_bypass(nodes, comment,
|
|
109
|
-
|
|
110
|
-
|
|
80
|
+
sig { params(nodes: T::Array[SemanticNode], comment: Parser::Source::Comment, bypass_reason: String).void }
|
|
81
|
+
def assign_bypass(nodes, comment, bypass_reason)
|
|
82
|
+
comment_loc = T.cast(comment.loc, Parser::Source::Map)
|
|
83
|
+
comment_line = T.cast(comment_loc.line, Integer)
|
|
111
84
|
|
|
112
|
-
innermost_node = nodes.reverse.find { |
|
|
113
|
-
innermost_node&.add_bypass(
|
|
85
|
+
innermost_node = nodes.reverse.find { |node| comment_line.between?(node.start_line - 1, node.end_line + 1) }
|
|
86
|
+
innermost_node&.add_bypass(bypass_reason)
|
|
114
87
|
end
|
|
115
88
|
|
|
116
89
|
sig do
|
|
@@ -138,8 +111,8 @@ module SimpleCov
|
|
|
138
111
|
const_node = T.cast(node.children[0], Parser::AST::Node)
|
|
139
112
|
const_node_name = T.cast(T.cast(const_node.loc, Parser::Source::Map::Constant).name, Parser::Source::Range)
|
|
140
113
|
name = T.cast(const_node_name.source, String)
|
|
141
|
-
|
|
142
|
-
[
|
|
114
|
+
new_context = context.empty? ? name : "#{context}#{NAMESPACE_SEPARATOR}#{name}"
|
|
115
|
+
[new_context, build_node(node, new_context, node.type.to_s.capitalize)]
|
|
143
116
|
end
|
|
144
117
|
|
|
145
118
|
sig do
|
|
@@ -148,8 +121,8 @@ module SimpleCov
|
|
|
148
121
|
end
|
|
149
122
|
def extract_instance_method_metadata(node, context)
|
|
150
123
|
name = T.cast(node.children.first, Symbol).to_s
|
|
151
|
-
|
|
152
|
-
[
|
|
124
|
+
new_context = context.empty? ? "#{INSTANCE_SEPARATOR}#{name}" : "#{context}#{INSTANCE_SEPARATOR}#{name}"
|
|
125
|
+
[new_context, build_node(node, new_context, TYPE_INSTANCE_METHOD)]
|
|
153
126
|
end
|
|
154
127
|
|
|
155
128
|
sig do
|
|
@@ -158,8 +131,8 @@ module SimpleCov
|
|
|
158
131
|
end
|
|
159
132
|
def extract_singleton_method_metadata(node, context)
|
|
160
133
|
name = T.cast(node.children[1], Symbol).to_s
|
|
161
|
-
|
|
162
|
-
[
|
|
134
|
+
new_context = context.empty? ? "#{SINGLETON_SEPARATOR}#{name}" : "#{context}#{SINGLETON_SEPARATOR}#{name}"
|
|
135
|
+
[new_context, build_node(node, new_context, TYPE_SINGLETON_METHOD)]
|
|
163
136
|
end
|
|
164
137
|
|
|
165
138
|
sig do
|
|
@@ -168,9 +141,9 @@ module SimpleCov
|
|
|
168
141
|
end
|
|
169
142
|
def build_node(node, name, type)
|
|
170
143
|
loc = T.cast(node.loc, Parser::Source::Map)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
SemanticNode.new(name: name, type: type, start_line:
|
|
144
|
+
start_line = T.cast(loc.line, Integer)
|
|
145
|
+
end_line = T.cast(loc.last_line, Integer)
|
|
146
|
+
SemanticNode.new(name: name, type: type, start_line: start_line, end_line: end_line, bypass_reasons: [])
|
|
174
147
|
end
|
|
175
148
|
end
|
|
176
149
|
end
|
|
@@ -10,6 +10,19 @@ module SimpleCov
|
|
|
10
10
|
class Configuration
|
|
11
11
|
extend T::Sig
|
|
12
12
|
|
|
13
|
+
# Default output path for the generated markdown report
|
|
14
|
+
DEFAULT_REPORT_PATH = T.let('coverage/ai_report.md', String)
|
|
15
|
+
# Default maximum size of the output file in kilobytes
|
|
16
|
+
DEFAULT_MAX_FILE_SIZE_KB = T.let(50, Integer)
|
|
17
|
+
# Default maximum number of lines for a single snippet
|
|
18
|
+
DEFAULT_MAX_SNIPPET_LINES = T.let(5, Integer)
|
|
19
|
+
# Default flag for outputting to console
|
|
20
|
+
DEFAULT_OUTPUT_TO_CONSOLE = T.let(false, T::Boolean)
|
|
21
|
+
# Default granularity level of the report
|
|
22
|
+
DEFAULT_GRANULARITY = T.let(:fine, Symbol)
|
|
23
|
+
# Default flag for including bypassed regions in the report
|
|
24
|
+
DEFAULT_INCLUDE_BYPASSES = T.let(true, T::Boolean)
|
|
25
|
+
|
|
13
26
|
# The absolute or relative system path where the final token-efficient markdown
|
|
14
27
|
# document acts as an artifact.
|
|
15
28
|
# @return [String]
|
|
@@ -44,12 +57,12 @@ module SimpleCov
|
|
|
44
57
|
|
|
45
58
|
sig { void }
|
|
46
59
|
def initialize
|
|
47
|
-
@report_path = T.let(
|
|
48
|
-
@max_file_size_kb = T.let(
|
|
49
|
-
@max_snippet_lines = T.let(
|
|
50
|
-
@output_to_console = T.let(
|
|
51
|
-
@granularity = T.let(
|
|
52
|
-
@include_bypasses = T.let(
|
|
60
|
+
@report_path = T.let(DEFAULT_REPORT_PATH, String)
|
|
61
|
+
@max_file_size_kb = T.let(DEFAULT_MAX_FILE_SIZE_KB, Integer)
|
|
62
|
+
@max_snippet_lines = T.let(DEFAULT_MAX_SNIPPET_LINES, Integer)
|
|
63
|
+
@output_to_console = T.let(DEFAULT_OUTPUT_TO_CONSOLE, T::Boolean)
|
|
64
|
+
@granularity = T.let(DEFAULT_GRANULARITY, Symbol)
|
|
65
|
+
@include_bypasses = T.let(DEFAULT_INCLUDE_BYPASSES, T::Boolean)
|
|
53
66
|
end
|
|
54
67
|
end
|
|
55
68
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
# Houses globally shared constant values utilized across the AI formatter suite
|
|
8
|
+
# to prevent magic string duplication and establish a single source of truth.
|
|
9
|
+
module Constants
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# The explicit value used to designate a file or coverage block as perfectly covered.
|
|
13
|
+
PERFECT_COVERAGE_PERCENT = T.let(100.0, Float)
|
|
14
|
+
|
|
15
|
+
# The directive typically employed within comments to force coverage engines to bypass execution tracking.
|
|
16
|
+
NOCOV_DIRECTIVE = T.let(':nocov:', String)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Enriches SimpleCov branch data with column information from raw coverage data.
|
|
9
|
+
class BranchEnricher
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { params(file: SimpleCov::SourceFile).void }
|
|
13
|
+
def self.enrich(file)
|
|
14
|
+
return unless file.respond_to?(:coverage_data)
|
|
15
|
+
|
|
16
|
+
case (cov = file.coverage_data)
|
|
17
|
+
when Hash
|
|
18
|
+
case (branches = cov['branches'])
|
|
19
|
+
when Hash
|
|
20
|
+
process_branches(file, branches)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
rescue StandardError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
extend T::Sig
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
sig { params(file: SimpleCov::SourceFile, branches: T::Hash[BasicObject, BasicObject]).void }
|
|
33
|
+
def process_branches(file, branches)
|
|
34
|
+
raw = extract_raw_branches(file, branches)
|
|
35
|
+
apply_column_data(file.branches, raw)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { params(file: SimpleCov::SourceFile, branches: T::Hash[BasicObject, BasicObject]).returns(T::Array[BasicObject]) }
|
|
39
|
+
def extract_raw_branches(file, branches)
|
|
40
|
+
branches.flat_map do |_condition, branch_hash|
|
|
41
|
+
case branch_hash
|
|
42
|
+
when Hash
|
|
43
|
+
branch_hash.map do |branch_data, _hit_count|
|
|
44
|
+
T.cast(file.send(:restore_ruby_data_structure, branch_data), BasicObject)
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
[]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { params(branches: T::Array[SimpleCov::SourceFile::Branch], raw_branches: T::Array[BasicObject]).void }
|
|
53
|
+
def apply_column_data(branches, raw_branches)
|
|
54
|
+
branches.zip(raw_branches).each do |branch, raw|
|
|
55
|
+
case raw
|
|
56
|
+
when Array
|
|
57
|
+
next unless raw.size >= 6
|
|
58
|
+
|
|
59
|
+
branch.instance_variable_set(:@start_col, T.cast(raw[3], Integer))
|
|
60
|
+
branch.instance_variable_set(:@end_col, T.cast(raw[5], Integer))
|
|
61
|
+
branch.class.send(:attr_reader, :start_col, :end_col) unless branch.respond_to?(:start_col)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -9,9 +9,21 @@ module SimpleCov
|
|
|
9
9
|
class BypassCompiler
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Section heading for bypassed coverage
|
|
13
|
+
HEADING = T.let("## Ignored Coverage Bypasses\n\n", String)
|
|
14
|
+
# Template for formatting file headers in the bypass section
|
|
15
|
+
FILE_HEADING_TEMPLATE = T.let('### `%s`', String)
|
|
16
|
+
# Template for detailing an individual bypass directive
|
|
17
|
+
BYPASS_TEMPLATE = T.let(
|
|
18
|
+
"- `%s`\n " \
|
|
19
|
+
'- **Bypass Present:** Contains `%s` directive artificially ' \
|
|
20
|
+
'ignoring coverage (Occurrence %d of %d).',
|
|
21
|
+
String
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
sig { params(coverage_metrics: SimpleCov::Result, builder: MarkdownBuilder).void }
|
|
25
|
+
def initialize(coverage_metrics, builder)
|
|
26
|
+
@coverage_metrics = coverage_metrics
|
|
15
27
|
@builder = builder
|
|
16
28
|
end
|
|
17
29
|
|
|
@@ -22,7 +34,7 @@ module SimpleCov
|
|
|
22
34
|
|
|
23
35
|
return unless has_bypasses
|
|
24
36
|
|
|
25
|
-
buffer.puts
|
|
37
|
+
buffer.puts HEADING
|
|
26
38
|
buffer.puts bypass_buffer.string
|
|
27
39
|
end
|
|
28
40
|
|
|
@@ -31,12 +43,12 @@ module SimpleCov
|
|
|
31
43
|
sig { params(buffer: StringIO).returns(T::Boolean) }
|
|
32
44
|
def compile_all_bypasses(buffer)
|
|
33
45
|
has_bypasses = T.let(false, T::Boolean)
|
|
34
|
-
T.let(@
|
|
35
|
-
|
|
36
|
-
next if
|
|
46
|
+
T.let(@coverage_metrics.files.to_a, T::Array[SimpleCov::SourceFile]).each do |file|
|
|
47
|
+
bypassed_nodes = fetch_bypassed_nodes(file.filename)
|
|
48
|
+
next if bypassed_nodes.empty?
|
|
37
49
|
|
|
38
50
|
has_bypasses = true
|
|
39
|
-
write_file_bypasses(buffer, file,
|
|
51
|
+
write_file_bypasses(buffer, file, bypassed_nodes)
|
|
40
52
|
end
|
|
41
53
|
has_bypasses
|
|
42
54
|
end
|
|
@@ -44,19 +56,17 @@ module SimpleCov
|
|
|
44
56
|
sig { params(filename: String).returns(T::Array[ASTResolver::SemanticNode]) }
|
|
45
57
|
def fetch_bypassed_nodes(filename)
|
|
46
58
|
nodes = @builder.try_resolve_ast(filename)
|
|
47
|
-
nodes ? nodes.select { |
|
|
59
|
+
nodes ? nodes.select { |node| node.bypass_reasons.any? } : []
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
sig do
|
|
51
|
-
params(buffer: StringIO, file: SimpleCov::SourceFile,
|
|
63
|
+
params(buffer: StringIO, file: SimpleCov::SourceFile, bypassed_nodes: T::Array[ASTResolver::SemanticNode]).void
|
|
52
64
|
end
|
|
53
|
-
def write_file_bypasses(buffer, file,
|
|
54
|
-
buffer.puts
|
|
55
|
-
total =
|
|
56
|
-
|
|
57
|
-
buffer.puts
|
|
58
|
-
'- **Bypass Present:** Contains `:nocov:` directive artificially ' \
|
|
59
|
-
"ignoring coverage (Occurrence #{idx + 1} of #{total})."
|
|
65
|
+
def write_file_bypasses(buffer, file, bypassed_nodes)
|
|
66
|
+
buffer.puts format(FILE_HEADING_TEMPLATE, file.project_filename)
|
|
67
|
+
total = bypassed_nodes.size
|
|
68
|
+
bypassed_nodes.each_with_index do |node, idx|
|
|
69
|
+
buffer.puts format(BYPASS_TEMPLATE, node.name, Constants::NOCOV_DIRECTIVE, idx + 1, total)
|
|
60
70
|
end
|
|
61
71
|
buffer.puts ''
|
|
62
72
|
end
|
|
@@ -10,22 +10,35 @@ module SimpleCov
|
|
|
10
10
|
extend T::Sig
|
|
11
11
|
include SnippetFormatter
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
# Header for the coverage deficits section
|
|
14
|
+
HEADING = T.let("## Coverage Deficits\n\n", String)
|
|
15
|
+
# Template for file-level deficit headings
|
|
16
|
+
FILE_HEADING_TEMPLATE = T.let('### `%s`', String)
|
|
17
|
+
# Error message for AST parsing failures
|
|
18
|
+
ERROR_AST_FAILED = T.let(" - **ERROR:** AST Parsing Failed. Showing raw line numbers instead.\n", String)
|
|
19
|
+
# Template for node-level deficit headings
|
|
20
|
+
NODE_HEADING_TEMPLATE = T.let('- `%s`', String)
|
|
21
|
+
# Coarse-grained deficit summary message
|
|
22
|
+
DEFICIT_COARSE = T.let(' - **Deficit:** Contains unexecuted lines or branches.', String)
|
|
23
|
+
# Template for a specific line deficit
|
|
24
|
+
LINE_DEFICIT_TEMPLATE = T.let(' - **Line Deficit:** [L%d] `%s` %s', String)
|
|
25
|
+
# Template for a specific branch deficit
|
|
26
|
+
BRANCH_DEFICIT_TEMPLATE = T.let(' - **Branch Deficit:** [L%s] Missing coverage for `%s` branch: `%s` %s',
|
|
27
|
+
String)
|
|
28
|
+
|
|
29
|
+
sig { params(coverage_metrics: SimpleCov::Result, config: Configuration, builder: MarkdownBuilder).void }
|
|
30
|
+
def initialize(coverage_metrics, config, builder)
|
|
31
|
+
@coverage_metrics = coverage_metrics
|
|
16
32
|
@config = config
|
|
17
33
|
@builder = builder
|
|
18
34
|
end
|
|
19
35
|
|
|
20
36
|
sig { params(buffer: StringIO).void }
|
|
21
37
|
def write_deficits(buffer)
|
|
22
|
-
files =
|
|
23
|
-
@result.files.reject { |f| f.covered_percent >= 100.0 }.sort_by { |f| [f.covered_percent, f.filename] },
|
|
24
|
-
T::Array[SimpleCov::SourceFile]
|
|
25
|
-
)
|
|
38
|
+
files = find_deficit_files
|
|
26
39
|
return if files.empty?
|
|
27
40
|
|
|
28
|
-
buffer.puts
|
|
41
|
+
buffer.puts HEADING
|
|
29
42
|
files.each do |file|
|
|
30
43
|
break if @builder.truncate_if_needed?
|
|
31
44
|
|
|
@@ -35,93 +48,59 @@ module SimpleCov
|
|
|
35
48
|
|
|
36
49
|
private
|
|
37
50
|
|
|
38
|
-
sig {
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
sig { returns(T::Array[SimpleCov::SourceFile]) }
|
|
52
|
+
def find_deficit_files
|
|
53
|
+
files_with_deficits = @coverage_metrics.files.reject do |f|
|
|
54
|
+
line_perfect?(f) && branch_perfect?(f)
|
|
55
|
+
end
|
|
56
|
+
T.let(files_with_deficits.sort_by { |file| [file.covered_percent, file.filename] }, T::Array[SimpleCov::SourceFile])
|
|
43
57
|
end
|
|
44
58
|
|
|
45
|
-
sig { params(
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
group = MarkdownBuilder::DeficitGroup.new(
|
|
49
|
-
lines: file.missed_lines,
|
|
50
|
-
branches: file.missed_branches
|
|
51
|
-
)
|
|
52
|
-
format_deficit_group(buffer, group, fetch_source_lines(file.filename))
|
|
53
|
-
buffer.puts ''
|
|
59
|
+
sig { params(file: SimpleCov::SourceFile).returns(T::Boolean) }
|
|
60
|
+
def line_perfect?(file)
|
|
61
|
+
file.covered_percent >= Constants::PERFECT_COVERAGE_PERCENT
|
|
54
62
|
end
|
|
55
63
|
|
|
56
|
-
sig
|
|
57
|
-
|
|
64
|
+
sig { params(file: SimpleCov::SourceFile).returns(T::Boolean) }
|
|
65
|
+
def branch_perfect?(file)
|
|
66
|
+
cov = file.respond_to?(:branches_coverage_percent) ? file.branches_coverage_percent : nil
|
|
67
|
+
branch_coverage_perfect?(cov)
|
|
58
68
|
end
|
|
59
|
-
def process_deficits(buffer, file, nodes)
|
|
60
|
-
node_deficits = DeficitGrouper.build(file, nodes)
|
|
61
|
-
source_lines = T.let(nil, T.nilable(T::Array[String]))
|
|
62
|
-
|
|
63
|
-
node_deficits.each do |node_name, group|
|
|
64
|
-
break if @builder.truncate_if_needed?
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
sig { params(coverage: T.nilable(BasicObject)).returns(T::Boolean) }
|
|
71
|
+
def branch_coverage_perfect?(coverage)
|
|
72
|
+
case coverage
|
|
73
|
+
when Float, Integer
|
|
74
|
+
coverage >= Constants::PERFECT_COVERAGE_PERCENT
|
|
75
|
+
else
|
|
76
|
+
case coverage
|
|
77
|
+
when nil then true
|
|
78
|
+
else false
|
|
79
|
+
end
|
|
68
80
|
end
|
|
69
|
-
|
|
70
|
-
buffer.puts ''
|
|
71
81
|
end
|
|
72
82
|
|
|
73
|
-
sig { params(buffer: StringIO,
|
|
74
|
-
def
|
|
75
|
-
|
|
83
|
+
sig { params(buffer: StringIO, file: SimpleCov::SourceFile).void }
|
|
84
|
+
def process_file(buffer, file)
|
|
85
|
+
BranchEnricher.enrich(file)
|
|
86
|
+
buffer.puts format(FILE_HEADING_TEMPLATE, file.project_filename)
|
|
87
|
+
|
|
88
|
+
formatter = DeficitFormatter.new(buffer, @config)
|
|
89
|
+
nodes = @builder.try_resolve_ast(file.filename)
|
|
76
90
|
|
|
77
|
-
if
|
|
78
|
-
|
|
91
|
+
if nodes
|
|
92
|
+
formatter.process_deficits(file, nodes, -> { safe_readlines(file.filename) })
|
|
79
93
|
else
|
|
80
|
-
|
|
94
|
+
formatter.format_raw_deficits(file, safe_readlines(file.filename))
|
|
81
95
|
end
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
sig { params(filename: String).returns(T::Array[String]) }
|
|
85
|
-
def
|
|
99
|
+
def safe_readlines(filename)
|
|
86
100
|
File.readlines(filename)
|
|
87
101
|
rescue StandardError
|
|
88
102
|
[]
|
|
89
103
|
end
|
|
90
|
-
|
|
91
|
-
sig { params(buffer: StringIO, group: DeficitGroup, source_lines: T::Array[String]).void }
|
|
92
|
-
def format_deficit_group(buffer, group, source_lines)
|
|
93
|
-
group.lines.each do |line|
|
|
94
|
-
write_line_snippet(buffer, line, source_lines, group.semantic_node)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
group.branches.each do |branch|
|
|
98
|
-
write_branch_snippet(buffer, branch, source_lines, group.semantic_node)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
sig do
|
|
103
|
-
params(buffer: StringIO, line: SimpleCov::SourceFile::Line, source_lines: T::Array[String],
|
|
104
|
-
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
105
|
-
end
|
|
106
|
-
def write_line_snippet(buffer, line, source_lines, node)
|
|
107
|
-
line_num = line.line_number
|
|
108
|
-
text = truncate_snippet(fetch_snippet_text([line_num], source_lines), @config.max_snippet_lines)
|
|
109
|
-
occurrence_str = calculate_occurrence(line_num, source_lines, node)
|
|
110
|
-
buffer.puts " - **Line Deficit:** `#{text}` #{occurrence_str}".rstrip
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
sig do
|
|
114
|
-
params(buffer: StringIO, branch: SimpleCov::SourceFile::Branch, source_lines: T::Array[String],
|
|
115
|
-
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
116
|
-
end
|
|
117
|
-
def write_branch_snippet(buffer, branch, source_lines, node)
|
|
118
|
-
start_line = branch.start_line
|
|
119
|
-
end_line = branch.end_line
|
|
120
|
-
lines_range = T.cast((start_line..end_line).to_a, T::Array[Integer])
|
|
121
|
-
text = truncate_snippet(fetch_snippet_text(lines_range, source_lines), @config.max_snippet_lines)
|
|
122
|
-
occurrence_str = calculate_occurrence(start_line, source_lines, node)
|
|
123
|
-
buffer.puts " - **Branch Deficit:** Missing coverage for conditional `#{text}` #{occurrence_str}".rstrip
|
|
124
|
-
end
|
|
125
104
|
end
|
|
126
105
|
end
|
|
127
106
|
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SimpleCov
|
|
5
|
+
module Formatter
|
|
6
|
+
class AIFormatter
|
|
7
|
+
class MarkdownBuilder
|
|
8
|
+
# Handles the formatting of line and branch deficits into markdown.
|
|
9
|
+
class DeficitFormatter
|
|
10
|
+
extend T::Sig
|
|
11
|
+
include SnippetFormatter
|
|
12
|
+
|
|
13
|
+
# Error message for AST parsing failures
|
|
14
|
+
ERROR_AST_FAILED = T.let(" - **ERROR:** AST Parsing Failed. Showing raw line numbers instead.\n", String)
|
|
15
|
+
# Template for node-level deficit headings
|
|
16
|
+
NODE_HEADING_TEMPLATE = T.let('- `%s`', String)
|
|
17
|
+
# Coarse-grained deficit summary message
|
|
18
|
+
DEFICIT_COARSE = T.let(' - **Deficit:** Contains unexecuted lines or branches.', String)
|
|
19
|
+
# Template for a specific line deficit
|
|
20
|
+
LINE_DEFICIT_TMPL = T.let(' - **Line Deficit:** [L%d] `%s` %s', String)
|
|
21
|
+
# Template for a specific branch deficit
|
|
22
|
+
BRANCH_DEFICIT_TMPL = T.let(' - **Branch Deficit:** [L%s] Missing coverage for `%s` branch: `%s` %s', String)
|
|
23
|
+
|
|
24
|
+
sig { params(buffer: StringIO, config: Configuration).void }
|
|
25
|
+
def initialize(buffer, config)
|
|
26
|
+
@buffer = buffer
|
|
27
|
+
@config = config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(file: SimpleCov::SourceFile, source_lines: T::Array[String]).void }
|
|
31
|
+
def format_raw_deficits(file, source_lines)
|
|
32
|
+
@buffer.puts ERROR_AST_FAILED
|
|
33
|
+
deficit_group = MarkdownBuilder::DeficitGroup.new(lines: file.missed_lines, branches: file.missed_branches)
|
|
34
|
+
format_deficit_group(deficit_group, source_lines)
|
|
35
|
+
@buffer.puts ''
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig do
|
|
39
|
+
params(file: SimpleCov::SourceFile, nodes: T::Array[ASTResolver::SemanticNode],
|
|
40
|
+
safe_readlines_proc: T.proc.returns(T::Array[String])).void
|
|
41
|
+
end
|
|
42
|
+
def process_deficits(file, nodes, safe_readlines_proc)
|
|
43
|
+
node_deficits = DeficitGrouper.build(file, nodes)
|
|
44
|
+
source_lines = T.let(nil, T.nilable(T::Array[String]))
|
|
45
|
+
|
|
46
|
+
node_deficits.each do |node_name, deficit_group|
|
|
47
|
+
source_lines ||= safe_readlines_proc.call
|
|
48
|
+
format_node_deficit(node_name, deficit_group, source_lines)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@buffer.puts ''
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
sig { params(node_name: String, deficit_group: DeficitGroup, source_lines: T::Array[String]).void }
|
|
57
|
+
def format_node_deficit(node_name, deficit_group, source_lines)
|
|
58
|
+
@buffer.puts format(NODE_HEADING_TEMPLATE, node_name)
|
|
59
|
+
|
|
60
|
+
if @config.granularity == :coarse
|
|
61
|
+
@buffer.puts DEFICIT_COARSE
|
|
62
|
+
else
|
|
63
|
+
format_deficit_group(deficit_group, source_lines)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { params(deficit_group: DeficitGroup, source_lines: T::Array[String]).void }
|
|
68
|
+
def format_deficit_group(deficit_group, source_lines)
|
|
69
|
+
deficit_group.lines.each do |line|
|
|
70
|
+
write_line_snippet(line, source_lines, deficit_group.semantic_node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
deficit_group.branches.each do |branch|
|
|
74
|
+
write_branch_snippet(branch, source_lines, deficit_group.semantic_node)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig do
|
|
79
|
+
params(line: SimpleCov::SourceFile::Line, source_lines: T::Array[String],
|
|
80
|
+
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
81
|
+
end
|
|
82
|
+
def write_line_snippet(line, source_lines, node)
|
|
83
|
+
text = truncate_snippet(fetch_snippet_text([line.line_number], source_lines), @config.max_snippet_lines)
|
|
84
|
+
occurrence_str = calculate_occurrence(line.line_number, source_lines, node)
|
|
85
|
+
@buffer.puts format(LINE_DEFICIT_TMPL, line.line_number, text, occurrence_str).rstrip
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig do
|
|
89
|
+
params(branch: SimpleCov::SourceFile::Branch, source_lines: T::Array[String],
|
|
90
|
+
node: T.nilable(ASTResolver::SemanticNode)).void
|
|
91
|
+
end
|
|
92
|
+
def write_branch_snippet(branch, source_lines, node)
|
|
93
|
+
text = truncate_snippet(extract_branch_text(branch, source_lines), @config.max_snippet_lines)
|
|
94
|
+
occurrence = calculate_occurrence(branch.start_line, source_lines, node)
|
|
95
|
+
label = format_branch_label(branch)
|
|
96
|
+
|
|
97
|
+
type_val = branch.respond_to?(:type) ? branch.type : nil
|
|
98
|
+
|
|
99
|
+
type_label = type_val ? type_val.to_s : 'conditional'
|
|
100
|
+
@buffer.puts format(BRANCH_DEFICIT_TMPL, label, type_label, text, occurrence).rstrip
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { params(branch: SimpleCov::SourceFile::Branch).returns(String) }
|
|
104
|
+
def format_branch_label(branch)
|
|
105
|
+
branch.start_line == branch.end_line ? branch.start_line.to_s : "#{branch.start_line}-#{branch.end_line}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(branch: SimpleCov::SourceFile::Branch, source_lines: T::Array[String]).returns(String) }
|
|
109
|
+
def extract_branch_text(branch, source_lines)
|
|
110
|
+
start_col = fetch_column(branch, :start_col)
|
|
111
|
+
end_col = fetch_column(branch, :end_col)
|
|
112
|
+
|
|
113
|
+
inline_text = extract_inline_branch(branch, start_col, end_col, source_lines)
|
|
114
|
+
return inline_text if inline_text
|
|
115
|
+
|
|
116
|
+
lines_range = T.cast((branch.start_line..branch.end_line).to_a, T::Array[Integer])
|
|
117
|
+
fetch_snippet_text(lines_range, source_lines)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(branch: SimpleCov::SourceFile::Branch, col: Symbol).returns(T.nilable(Integer)) }
|
|
121
|
+
def fetch_column(branch, col)
|
|
122
|
+
val = branch.respond_to?(col) ? branch.public_send(col) : branch.instance_variable_get(:"@#{col}")
|
|
123
|
+
T.cast(val, T.nilable(Integer))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig do
|
|
127
|
+
params(branch: SimpleCov::SourceFile::Branch, start_col: T.nilable(Integer),
|
|
128
|
+
end_col: T.nilable(Integer), source_lines: T::Array[String]).returns(T.nilable(String))
|
|
129
|
+
end
|
|
130
|
+
def extract_inline_branch(branch, start_col, end_col, source_lines)
|
|
131
|
+
return nil unless branch.start_line == branch.end_line && start_col && end_col
|
|
132
|
+
|
|
133
|
+
line_text = source_lines[branch.start_line - 1]
|
|
134
|
+
return nil unless line_text && line_text.length >= end_col
|
|
135
|
+
|
|
136
|
+
line_text[start_col...end_col].to_s.strip
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -9,6 +9,11 @@ module SimpleCov
|
|
|
9
9
|
class DeficitGrouper
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
+
# Fallback identifier format for unassociated lines
|
|
13
|
+
FALLBACK_LINE_NAME = T.let('Line %d', String)
|
|
14
|
+
# Fallback identifier format for unassociated branches
|
|
15
|
+
FALLBACK_BRANCH_NAME = T.let('Lines %d-%d', String)
|
|
16
|
+
|
|
12
17
|
sig { returns(T::Hash[String, DeficitGroup]) }
|
|
13
18
|
attr_reader :node_deficits
|
|
14
19
|
|
|
@@ -32,11 +37,11 @@ module SimpleCov
|
|
|
32
37
|
sig { returns(T::Hash[String, DeficitGroup]) }
|
|
33
38
|
def sort_deficits
|
|
34
39
|
T.let(
|
|
35
|
-
@node_deficits.sort_by do |
|
|
36
|
-
if (
|
|
37
|
-
|
|
40
|
+
@node_deficits.sort_by do |node_name, deficit_group|
|
|
41
|
+
if (semantic_node = deficit_group.semantic_node)
|
|
42
|
+
semantic_node.start_line
|
|
38
43
|
else
|
|
39
|
-
|
|
44
|
+
node_name.match(/\d+/)&.to_s&.to_i || Float::INFINITY
|
|
40
45
|
end
|
|
41
46
|
end.to_h,
|
|
42
47
|
T::Hash[String, DeficitGroup]
|
|
@@ -53,9 +58,9 @@ module SimpleCov
|
|
|
53
58
|
sig { params(line: SimpleCov::SourceFile::Line).void }
|
|
54
59
|
def add_missed_line(line)
|
|
55
60
|
line_num = line.line_number
|
|
56
|
-
|
|
57
|
-
node_name =
|
|
58
|
-
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node:
|
|
61
|
+
matched_node = @nodes.reverse.find { |node| line_num.between?(node.start_line, node.end_line) }
|
|
62
|
+
node_name = matched_node ? matched_node.name : format(FALLBACK_LINE_NAME, line_num)
|
|
63
|
+
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: matched_node)
|
|
59
64
|
T.must(@node_deficits[node_name]).lines << line
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -72,9 +77,11 @@ module SimpleCov
|
|
|
72
77
|
def add_missed_branch(branch)
|
|
73
78
|
start_line = branch.start_line
|
|
74
79
|
end_line = branch.end_line
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
matched_node = @nodes.reverse.find do |node|
|
|
81
|
+
start_line >= node.start_line && end_line <= node.end_line
|
|
82
|
+
end
|
|
83
|
+
node_name = matched_node ? matched_node.name : format(FALLBACK_BRANCH_NAME, start_line, end_line)
|
|
84
|
+
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: matched_node)
|
|
78
85
|
T.must(@node_deficits[node_name]).branches << branch
|
|
79
86
|
end
|
|
80
87
|
end
|
|
@@ -9,6 +9,13 @@ module SimpleCov
|
|
|
9
9
|
module SnippetFormatter
|
|
10
10
|
extend T::Sig
|
|
11
11
|
|
|
12
|
+
# Approximate maximum characters per line for truncation calculation
|
|
13
|
+
ESTIMATED_CHARS_PER_LINE = T.let(80, Integer)
|
|
14
|
+
# Suffix added to truncated snippets
|
|
15
|
+
TRUNCATION_ELLIPSIS = T.let('...', String)
|
|
16
|
+
# Template for identical snippet occurrences indicator
|
|
17
|
+
OCCURRENCE_TEMPLATE = T.let('(Occurrence %d of %d).', String)
|
|
18
|
+
|
|
12
19
|
# Extracts and normalizes exact string literals from the source file arrays.
|
|
13
20
|
#
|
|
14
21
|
# @param line_nums [Array<Integer>] Target line coordinates.
|
|
@@ -16,18 +23,22 @@ module SimpleCov
|
|
|
16
23
|
# @return [String] Joined snippet text.
|
|
17
24
|
sig { params(line_nums: T::Array[Integer], source_lines: T::Array[String]).returns(String) }
|
|
18
25
|
def fetch_snippet_text(line_nums, source_lines)
|
|
19
|
-
line_nums.filter_map { |
|
|
26
|
+
line_nums.filter_map { |line_number| source_lines[line_number - 1]&.strip }.reject(&:empty?).join(' ')
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
# Safely limits the character length of a code snippet according to global configurations.
|
|
23
30
|
#
|
|
24
|
-
# @param
|
|
31
|
+
# @param snippet_text [String] The snippet to potentially truncate.
|
|
25
32
|
# @param max_snippet_lines [Integer] The configured max lines.
|
|
26
33
|
# @return [String] Truncated string with trailing ellipses if limit exceeded.
|
|
27
|
-
sig { params(
|
|
28
|
-
def truncate_snippet(
|
|
29
|
-
max_chars = max_snippet_lines *
|
|
30
|
-
|
|
34
|
+
sig { params(snippet_text: String, max_snippet_lines: Integer).returns(String) }
|
|
35
|
+
def truncate_snippet(snippet_text, max_snippet_lines)
|
|
36
|
+
max_chars = max_snippet_lines * ESTIMATED_CHARS_PER_LINE
|
|
37
|
+
if snippet_text.length > max_chars
|
|
38
|
+
"#{snippet_text[0...max_chars]}#{TRUNCATION_ELLIPSIS}"
|
|
39
|
+
else
|
|
40
|
+
snippet_text
|
|
41
|
+
end
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
# Disambiguates identical code snippets within the same semantic block (e.g., "(Occurrence 2 of 3)").
|
|
@@ -48,23 +59,23 @@ module SimpleCov
|
|
|
48
59
|
|
|
49
60
|
occurrences, current = count_snippet_occurrences(first_line_of_snippet, line_num, source_lines, node)
|
|
50
61
|
|
|
51
|
-
occurrences > 1 ?
|
|
62
|
+
occurrences > 1 ? Kernel.format(OCCURRENCE_TEMPLATE, current, occurrences) : ''
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
sig do
|
|
55
|
-
params(snippet: String,
|
|
66
|
+
params(snippet: String, target_line_number: Integer, source_lines: T::Array[String],
|
|
56
67
|
node: ASTResolver::SemanticNode).returns([Integer, Integer])
|
|
57
68
|
end
|
|
58
|
-
def count_snippet_occurrences(snippet,
|
|
69
|
+
def count_snippet_occurrences(snippet, target_line_number, source_lines, node)
|
|
59
70
|
occurrences = 0
|
|
60
71
|
current_occurrence = 1
|
|
61
72
|
|
|
62
|
-
(node.start_line..node.end_line).each do |
|
|
63
|
-
line_content = source_lines[
|
|
73
|
+
(node.start_line..node.end_line).each do |line_number|
|
|
74
|
+
line_content = source_lines[line_number - 1]&.strip
|
|
64
75
|
next unless line_content == snippet
|
|
65
76
|
|
|
66
77
|
occurrences += 1
|
|
67
|
-
current_occurrence = occurrences if
|
|
78
|
+
current_occurrence = occurrences if line_number == target_line_number
|
|
68
79
|
end
|
|
69
80
|
|
|
70
81
|
[occurrences, current_occurrence]
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
require_relative 'ast_resolver'
|
|
5
5
|
require 'time'
|
|
6
|
+
require 'stringio'
|
|
6
7
|
require_relative 'markdown_builder/snippet_formatter'
|
|
7
8
|
require_relative 'markdown_builder/bypass_compiler'
|
|
8
9
|
require_relative 'markdown_builder/deficit_grouper'
|
|
10
|
+
require_relative 'markdown_builder/branch_enricher'
|
|
11
|
+
require_relative 'markdown_builder/deficit_formatter'
|
|
9
12
|
require_relative 'markdown_builder/deficit_compiler'
|
|
10
13
|
|
|
11
14
|
module SimpleCov
|
|
@@ -18,6 +21,35 @@ module SimpleCov
|
|
|
18
21
|
extend T::Sig
|
|
19
22
|
include SnippetFormatter
|
|
20
23
|
|
|
24
|
+
# The number of bytes in a kilobyte
|
|
25
|
+
BYTES_PER_KB = T.let(1024.0, Float)
|
|
26
|
+
# Text representation for a passed coverage check
|
|
27
|
+
STATUS_PASSED = T.let('PASSED', String)
|
|
28
|
+
# Text representation for a failed coverage check
|
|
29
|
+
STATUS_FAILED = T.let('FAILED', String)
|
|
30
|
+
|
|
31
|
+
# Template for the report header
|
|
32
|
+
HEADER_TEMPLATE = T.let(
|
|
33
|
+
"# AI Coverage Digest\n" \
|
|
34
|
+
"**Status:** %<status>s\n" \
|
|
35
|
+
"**Global Line Coverage:** %<line_pct>s%%\n" \
|
|
36
|
+
"**Global Branch Coverage:** %<branch_pct>s%%\n" \
|
|
37
|
+
"**Generated At:** %<time>s (Local Timezone)\n",
|
|
38
|
+
String
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Alert heading for truncated reports
|
|
42
|
+
TRUNCATION_ALERT_HEADING = T.let('> **[WARNING] TRUNCATION NOTIFICATION:**', String)
|
|
43
|
+
# Alert body for truncated reports
|
|
44
|
+
TRUNCATION_ALERT_BODY = T.let(
|
|
45
|
+
'> The total coverage deficit report exceeded the maximum token ' \
|
|
46
|
+
'constraint (%<limit>d kB). ' \
|
|
47
|
+
'The report was truncated. The deficits detailed above represent ' \
|
|
48
|
+
'the lowest-coverage (most critical) files. ' \
|
|
49
|
+
'Please resolve these deficits to reveal the remaining uncovered files in subsequent test runs.',
|
|
50
|
+
String
|
|
51
|
+
)
|
|
52
|
+
|
|
21
53
|
# Groups unexecuted lines and branches under their common semantic node.
|
|
22
54
|
class DeficitGroup < T::Struct
|
|
23
55
|
# @return [ASTResolver::SemanticNode, nil] The corresponding structural boundary
|
|
@@ -30,11 +62,11 @@ module SimpleCov
|
|
|
30
62
|
|
|
31
63
|
# Initializes the Markdown sequence compilation.
|
|
32
64
|
#
|
|
33
|
-
# @param
|
|
65
|
+
# @param coverage_metrics [SimpleCov::Result] Application-wide coverage aggregation metrics
|
|
34
66
|
# @param config [Configuration] Pre-registered runtime toggles
|
|
35
|
-
sig { params(
|
|
36
|
-
def initialize(
|
|
37
|
-
@
|
|
67
|
+
sig { params(coverage_metrics: SimpleCov::Result, config: Configuration).void }
|
|
68
|
+
def initialize(coverage_metrics, config)
|
|
69
|
+
@coverage_metrics = T.let(coverage_metrics, SimpleCov::Result)
|
|
38
70
|
@config = T.let(config, Configuration)
|
|
39
71
|
@buffer = T.let(StringIO.new, StringIO)
|
|
40
72
|
@file_count = T.let(0, Integer)
|
|
@@ -49,8 +81,8 @@ module SimpleCov
|
|
|
49
81
|
sig { returns(String) }
|
|
50
82
|
def build
|
|
51
83
|
write_header
|
|
52
|
-
DeficitCompiler.new(@
|
|
53
|
-
BypassCompiler.new(@
|
|
84
|
+
DeficitCompiler.new(@coverage_metrics, @config, self).write_deficits(@buffer)
|
|
85
|
+
BypassCompiler.new(@coverage_metrics, self).write_bypasses(@buffer) if @config.include_bypasses
|
|
54
86
|
write_truncation_warning if @truncated
|
|
55
87
|
@buffer.string
|
|
56
88
|
end
|
|
@@ -64,7 +96,7 @@ module SimpleCov
|
|
|
64
96
|
|
|
65
97
|
sig { returns(T::Boolean) }
|
|
66
98
|
def truncate_if_needed?
|
|
67
|
-
return false unless @buffer.size /
|
|
99
|
+
return false unless @buffer.size / BYTES_PER_KB > @config.max_file_size_kb
|
|
68
100
|
|
|
69
101
|
@truncated = true
|
|
70
102
|
true
|
|
@@ -75,36 +107,36 @@ module SimpleCov
|
|
|
75
107
|
# Writes the summary header containing global coverage percentages and generation metadata.
|
|
76
108
|
sig { void }
|
|
77
109
|
def write_header
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@buffer.puts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
covered_pct = @coverage_metrics.covered_percent
|
|
111
|
+
status = covered_pct >= Constants::PERFECT_COVERAGE_PERCENT ? STATUS_PASSED : STATUS_FAILED
|
|
112
|
+
@buffer.puts format(
|
|
113
|
+
HEADER_TEMPLATE,
|
|
114
|
+
status: status,
|
|
115
|
+
line_pct: covered_pct.round(1),
|
|
116
|
+
branch_pct: calculate_branch_pct.round(1),
|
|
117
|
+
time: Time.now.iso8601
|
|
118
|
+
)
|
|
85
119
|
end
|
|
86
120
|
|
|
87
121
|
sig { returns(Float) }
|
|
88
122
|
def calculate_branch_pct
|
|
89
|
-
|
|
123
|
+
unless @coverage_metrics.respond_to?(:covered_branches) &&
|
|
124
|
+
@coverage_metrics.respond_to?(:total_branches)
|
|
125
|
+
return 0.0
|
|
126
|
+
end
|
|
90
127
|
|
|
91
|
-
total = @
|
|
128
|
+
total = @coverage_metrics.total_branches
|
|
92
129
|
return 0.0 if total.to_i.zero?
|
|
93
130
|
|
|
94
|
-
covered = @
|
|
95
|
-
covered.to_f / total *
|
|
131
|
+
covered = @coverage_metrics.covered_branches
|
|
132
|
+
covered.to_f / total * Constants::PERFECT_COVERAGE_PERCENT
|
|
96
133
|
end
|
|
97
134
|
|
|
98
135
|
# Appends a critical alert if the output hit the token-ceiling constraint and was forcibly terminated.
|
|
99
136
|
sig { void }
|
|
100
137
|
def write_truncation_warning
|
|
101
|
-
@buffer.puts
|
|
102
|
-
|
|
103
|
-
"constraint (#{@config.max_file_size_kb} kB). " \
|
|
104
|
-
'The report was truncated. The deficits detailed above represent ' \
|
|
105
|
-
'the lowest-coverage (most critical) files. ' \
|
|
106
|
-
'Please resolve these deficits to reveal the remaining uncovered files in subsequent test runs.'
|
|
107
|
-
@buffer.puts msg
|
|
138
|
+
@buffer.puts TRUNCATION_ALERT_HEADING
|
|
139
|
+
@buffer.puts format(TRUNCATION_ALERT_BODY, limit: @config.max_file_size_kb)
|
|
108
140
|
end
|
|
109
141
|
end
|
|
110
142
|
end
|
data/lib/simplecov-ai/version.rb
CHANGED
data/lib/simplecov-ai.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'sorbet-runtime'
|
|
|
5
5
|
require 'simplecov'
|
|
6
6
|
require 'parser/current'
|
|
7
7
|
|
|
8
|
+
require_relative 'simplecov-ai/constants'
|
|
8
9
|
require_relative 'simplecov-ai/version'
|
|
9
10
|
require_relative 'simplecov-ai/configuration'
|
|
10
11
|
require_relative 'simplecov-ai/ast_resolver'
|
|
@@ -21,6 +22,9 @@ module SimpleCov
|
|
|
21
22
|
class AIFormatter
|
|
22
23
|
extend T::Sig
|
|
23
24
|
|
|
25
|
+
# The standard output prefix used when reporting generation success.
|
|
26
|
+
SUCCESS_LOG_PREFIX = T.let("\n[SimpleCov AI Formatter] Digest written to ", String)
|
|
27
|
+
|
|
24
28
|
# Retrieves the global configuration for the AI formatter.
|
|
25
29
|
# The instantiation pattern ensures that defaults are securely lazily loaded
|
|
26
30
|
# before any coverage processing begins.
|
|
@@ -46,18 +50,18 @@ module SimpleCov
|
|
|
46
50
|
# directory structures, and securely persists the result to disk to guarantee
|
|
47
51
|
# an idempotent and strictly defined output location.
|
|
48
52
|
#
|
|
49
|
-
# @param
|
|
53
|
+
# @param coverage_metrics [SimpleCov::Result] The test coverage outcome generated by SimpleCov.
|
|
50
54
|
# @return [void]
|
|
51
|
-
sig { params(
|
|
52
|
-
def format(
|
|
55
|
+
sig { params(coverage_metrics: SimpleCov::Result).void }
|
|
56
|
+
def format(coverage_metrics)
|
|
53
57
|
config = self.class.configuration
|
|
54
|
-
builder = MarkdownBuilder.new(
|
|
58
|
+
builder = MarkdownBuilder.new(coverage_metrics, config)
|
|
55
59
|
digest = builder.build
|
|
56
60
|
|
|
57
61
|
FileUtils.mkdir_p(File.dirname(config.report_path))
|
|
58
62
|
File.write(config.report_path, digest)
|
|
59
63
|
|
|
60
|
-
puts "
|
|
64
|
+
puts "#{SUCCESS_LOG_PREFIX}#{config.report_path}" if config.output_to_console
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simplecov-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vitalii Lazebnyi
|
|
@@ -315,10 +315,14 @@ files:
|
|
|
315
315
|
- certs/simplecov-ai-public_cert.pem
|
|
316
316
|
- lib/simplecov-ai.rb
|
|
317
317
|
- lib/simplecov-ai/ast_resolver.rb
|
|
318
|
+
- lib/simplecov-ai/ast_resolver/semantic_node.rb
|
|
318
319
|
- lib/simplecov-ai/configuration.rb
|
|
320
|
+
- lib/simplecov-ai/constants.rb
|
|
319
321
|
- lib/simplecov-ai/markdown_builder.rb
|
|
322
|
+
- lib/simplecov-ai/markdown_builder/branch_enricher.rb
|
|
320
323
|
- lib/simplecov-ai/markdown_builder/bypass_compiler.rb
|
|
321
324
|
- lib/simplecov-ai/markdown_builder/deficit_compiler.rb
|
|
325
|
+
- lib/simplecov-ai/markdown_builder/deficit_formatter.rb
|
|
322
326
|
- lib/simplecov-ai/markdown_builder/deficit_grouper.rb
|
|
323
327
|
- lib/simplecov-ai/markdown_builder/snippet_formatter.rb
|
|
324
328
|
- lib/simplecov-ai/version.rb
|
|
@@ -343,7 +347,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
343
347
|
- !ruby/object:Gem::Version
|
|
344
348
|
version: '0'
|
|
345
349
|
requirements: []
|
|
346
|
-
rubygems_version: 4.0.
|
|
350
|
+
rubygems_version: 4.0.10
|
|
347
351
|
specification_version: 4
|
|
348
352
|
summary: An AI-optimized Markdown formatter for SimpleCov utilizing AST mapping.
|
|
349
353
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|