simplecov-ai 0.10.3 → 0.10.4
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.rb +52 -44
- data/lib/simplecov-ai/markdown_builder/bypass_compiler.rb +5 -2
- data/lib/simplecov-ai/markdown_builder/deficit_compiler.rb +1 -1
- data/lib/simplecov-ai/markdown_builder/deficit_grouper.rb +26 -7
- data/lib/simplecov-ai/markdown_builder/snippet_formatter.rb +1 -1
- data/lib/simplecov-ai/markdown_builder.rb +12 -7
- data/lib/simplecov-ai/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -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: 21fb51a293aef52967848c9b4fcc176b83aa274c347fb29abeaf76a34f040754
|
|
4
|
+
data.tar.gz: b43d99c3b4064c5e2914be0df4251774f78f0f6fa41cc51ae85c4e4547a586e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 55f0042ff2503ffc573c39a1393a407e66988e2de5f3e79d5d35eb539f70645bb1bcd6374870d84682dba430a87b7b0f9601baa446e9beb99f1e9e7aa3c45cf1
|
|
7
|
+
data.tar.gz: 2a94566df43e8bdd3c8e68ffcbe95bbda0683be0cd661313ede1af5a421d0972ad03d3b0363cf72259d631e80a490529075adc2b13df58a02d7ffa4c777ef95c
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -43,6 +43,11 @@ module SimpleCov
|
|
|
43
43
|
@end_line = end_line
|
|
44
44
|
@bypasses = bypasses
|
|
45
45
|
end
|
|
46
|
+
|
|
47
|
+
sig { params(bypass: String).void }
|
|
48
|
+
def add_bypass(bypass)
|
|
49
|
+
@bypasses << bypass
|
|
50
|
+
end
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
# Orchestrates the initial mapping algorithm on a target file to extract structural
|
|
@@ -54,115 +59,118 @@ module SimpleCov
|
|
|
54
59
|
def self.resolve(file_path)
|
|
55
60
|
return [] unless File.exist?(file_path)
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
source = File.read(file_path)
|
|
63
|
+
ast, comments = Parser::CurrentRuby.parse_with_comments(source)
|
|
64
|
+
|
|
65
|
+
resolver = new
|
|
66
|
+
nodes = resolver.traverse(ast)
|
|
67
|
+
resolver.assign_bypasses(nodes, comments)
|
|
68
|
+
nodes
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
# Recursively navigates an abstract node hierarchy, building SemanticNodes mappings
|
|
67
72
|
# around modules, classes, singleton, and instance methods while aggregating parent paths.
|
|
68
73
|
#
|
|
69
74
|
# @param node [Parser::AST::Node] The root AST node from which traversal executes.
|
|
70
|
-
# @param comments [Array<Parser::Source::Comment>] Lexical comments corresponding to nodes.
|
|
71
75
|
# @param context [String] An accumulated identifier linking namespaces to inner entities.
|
|
72
76
|
# @return [Array<SemanticNode>] Accumulation of all sub-tree defined endpoints.
|
|
73
77
|
sig do
|
|
74
|
-
params(node: Parser::AST::Node,
|
|
78
|
+
params(node: T.nilable(Parser::AST::Node),
|
|
75
79
|
context: String).returns(T::Array[SemanticNode])
|
|
76
80
|
end
|
|
77
|
-
def traverse(node,
|
|
81
|
+
def traverse(node, context = '')
|
|
78
82
|
return [] unless node.is_a?(Parser::AST::Node)
|
|
79
83
|
|
|
80
84
|
nodes = T.let([], T::Array[SemanticNode])
|
|
81
|
-
current_context, semantic_node = extract_node_metadata(node,
|
|
85
|
+
current_context, semantic_node = extract_node_metadata(node, context)
|
|
82
86
|
nodes << semantic_node if semantic_node
|
|
83
87
|
|
|
84
88
|
node.children.grep(Parser::AST::Node).each do |child|
|
|
85
|
-
nodes.concat(traverse(child,
|
|
89
|
+
nodes.concat(traverse(child, current_context))
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
nodes
|
|
89
93
|
end
|
|
90
94
|
|
|
95
|
+
sig { params(nodes: T::Array[SemanticNode], comments: T::Array[Parser::Source::Comment]).void }
|
|
96
|
+
def assign_bypasses(nodes, comments)
|
|
97
|
+
comments.each do |c|
|
|
98
|
+
c_text = T.cast(c.text, String)
|
|
99
|
+
next unless c_text.include?(':nocov:')
|
|
100
|
+
|
|
101
|
+
assign_bypass(nodes, c, c_text.strip)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
private
|
|
92
106
|
|
|
107
|
+
sig { params(nodes: T::Array[SemanticNode], comment: Parser::Source::Comment, text: String).void }
|
|
108
|
+
def assign_bypass(nodes, comment, text)
|
|
109
|
+
c_loc = T.cast(comment.loc, Parser::Source::Map)
|
|
110
|
+
c_line = T.cast(c_loc.line, Integer)
|
|
111
|
+
|
|
112
|
+
innermost_node = nodes.reverse.find { |n| c_line.between?(n.start_line - 1, n.end_line + 1) }
|
|
113
|
+
innermost_node&.add_bypass(text)
|
|
114
|
+
end
|
|
115
|
+
|
|
93
116
|
sig do
|
|
94
|
-
params(node: Parser::AST::Node,
|
|
117
|
+
params(node: Parser::AST::Node, context: String)
|
|
95
118
|
.returns([String, T.nilable(SemanticNode)])
|
|
96
119
|
end
|
|
97
|
-
def extract_node_metadata(node,
|
|
120
|
+
def extract_node_metadata(node, context)
|
|
98
121
|
case node.type
|
|
99
122
|
when :class, :module
|
|
100
|
-
extract_class_metadata(node,
|
|
123
|
+
extract_class_metadata(node, context)
|
|
101
124
|
when :def
|
|
102
|
-
extract_instance_method_metadata(node,
|
|
125
|
+
extract_instance_method_metadata(node, context)
|
|
103
126
|
when :defs
|
|
104
|
-
extract_singleton_method_metadata(node,
|
|
127
|
+
extract_singleton_method_metadata(node, context)
|
|
105
128
|
else
|
|
106
129
|
[context, nil]
|
|
107
130
|
end
|
|
108
131
|
end
|
|
109
132
|
|
|
110
133
|
sig do
|
|
111
|
-
params(node: Parser::AST::Node,
|
|
134
|
+
params(node: Parser::AST::Node, context: String)
|
|
112
135
|
.returns([String, T.nilable(SemanticNode)])
|
|
113
136
|
end
|
|
114
|
-
def extract_class_metadata(node,
|
|
137
|
+
def extract_class_metadata(node, context)
|
|
115
138
|
const_node = T.cast(node.children[0], Parser::AST::Node)
|
|
116
139
|
const_node_name = T.cast(T.cast(const_node.loc, Parser::Source::Map::Constant).name, Parser::Source::Range)
|
|
117
140
|
name = T.cast(const_node_name.source, String)
|
|
118
141
|
ctx = context.empty? ? name : "#{context}::#{name}"
|
|
119
|
-
[ctx, build_node(node,
|
|
142
|
+
[ctx, build_node(node, ctx, node.type.to_s.capitalize)]
|
|
120
143
|
end
|
|
121
144
|
|
|
122
145
|
sig do
|
|
123
|
-
params(node: Parser::AST::Node,
|
|
146
|
+
params(node: Parser::AST::Node, context: String)
|
|
124
147
|
.returns([String, T.nilable(SemanticNode)])
|
|
125
148
|
end
|
|
126
|
-
def extract_instance_method_metadata(node,
|
|
149
|
+
def extract_instance_method_metadata(node, context)
|
|
127
150
|
name = T.cast(node.children.first, Symbol).to_s
|
|
128
151
|
ctx = context.empty? ? "##{name}" : "#{context}##{name}"
|
|
129
|
-
[ctx, build_node(node,
|
|
152
|
+
[ctx, build_node(node, ctx, 'Instance Method')]
|
|
130
153
|
end
|
|
131
154
|
|
|
132
155
|
sig do
|
|
133
|
-
params(node: Parser::AST::Node,
|
|
156
|
+
params(node: Parser::AST::Node, context: String)
|
|
134
157
|
.returns([String, T.nilable(SemanticNode)])
|
|
135
158
|
end
|
|
136
|
-
def extract_singleton_method_metadata(node,
|
|
159
|
+
def extract_singleton_method_metadata(node, context)
|
|
137
160
|
name = T.cast(node.children[1], Symbol).to_s
|
|
138
161
|
ctx = context.empty? ? ".#{name}" : "#{context}.#{name}"
|
|
139
|
-
[ctx, build_node(node,
|
|
162
|
+
[ctx, build_node(node, ctx, 'Singleton Method')]
|
|
140
163
|
end
|
|
141
164
|
|
|
142
165
|
sig do
|
|
143
|
-
params(node: Parser::AST::Node,
|
|
166
|
+
params(node: Parser::AST::Node, name: String,
|
|
144
167
|
type: String).returns(SemanticNode)
|
|
145
168
|
end
|
|
146
|
-
def build_node(node,
|
|
169
|
+
def build_node(node, name, type)
|
|
147
170
|
loc = T.cast(node.loc, Parser::Source::Map)
|
|
148
171
|
start_ln = T.cast(loc.line, Integer)
|
|
149
172
|
end_ln = T.cast(loc.last_line, Integer)
|
|
150
|
-
|
|
151
|
-
SemanticNode.new(name: name, type: type, start_line: start_ln, end_line: end_ln, bypasses: bypasses)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
sig do
|
|
155
|
-
params(comments: T::Array[Parser::Source::Comment], start_line: Integer, end_line: Integer)
|
|
156
|
-
.returns(T::Array[String])
|
|
157
|
-
end
|
|
158
|
-
def extract_bypasses(comments, start_line, end_line)
|
|
159
|
-
matched = comments.select do |c|
|
|
160
|
-
c_loc = T.cast(c.loc, Parser::Source::Map)
|
|
161
|
-
c_line = T.cast(c_loc.line, Integer)
|
|
162
|
-
c_text = T.cast(c.text, String)
|
|
163
|
-
c_line.between?(start_line - 1, end_line + 1) && c_text.include?(':nocov:')
|
|
164
|
-
end
|
|
165
|
-
matched.map { |c| T.cast(c.text, String).strip }
|
|
173
|
+
SemanticNode.new(name: name, type: type, start_line: start_ln, end_line: end_ln, bypasses: [])
|
|
166
174
|
end
|
|
167
175
|
end
|
|
168
176
|
end
|
|
@@ -52,8 +52,11 @@ module SimpleCov
|
|
|
52
52
|
end
|
|
53
53
|
def write_file_bypasses(buffer, file, bypasses)
|
|
54
54
|
buffer.puts "### `#{file.project_filename}`"
|
|
55
|
-
bypasses.
|
|
56
|
-
|
|
55
|
+
total = bypasses.size
|
|
56
|
+
bypasses.each_with_index do |node, idx|
|
|
57
|
+
buffer.puts "- `#{node.name}`\n " \
|
|
58
|
+
'- **Bypass Present:** Contains `:nocov:` directive artificially ' \
|
|
59
|
+
"ignoring coverage (Occurrence #{idx + 1} of #{total})."
|
|
57
60
|
end
|
|
58
61
|
buffer.puts ''
|
|
59
62
|
end
|
|
@@ -107,7 +107,7 @@ module SimpleCov
|
|
|
107
107
|
line_num = line.line_number
|
|
108
108
|
text = truncate_snippet(fetch_snippet_text([line_num], source_lines), @config.max_snippet_lines)
|
|
109
109
|
occurrence_str = calculate_occurrence(line_num, source_lines, node)
|
|
110
|
-
buffer.puts " - **Line Deficit:**
|
|
110
|
+
buffer.puts " - **Line Deficit:** `#{text}` #{occurrence_str}".rstrip
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
sig do
|
|
@@ -26,20 +26,39 @@ module SimpleCov
|
|
|
26
26
|
grouper = new(nodes)
|
|
27
27
|
grouper.group_missed_lines(file)
|
|
28
28
|
grouper.group_missed_branches(file)
|
|
29
|
-
grouper.
|
|
29
|
+
grouper.sort_deficits
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Hash[String, DeficitGroup]) }
|
|
33
|
+
def sort_deficits
|
|
34
|
+
T.let(
|
|
35
|
+
@node_deficits.sort_by do |name, group|
|
|
36
|
+
if (node = group.semantic_node)
|
|
37
|
+
node.start_line
|
|
38
|
+
else
|
|
39
|
+
name.match(/\d+/)&.to_s&.to_i || Float::INFINITY
|
|
40
|
+
end
|
|
41
|
+
end.to_h,
|
|
42
|
+
T::Hash[String, DeficitGroup]
|
|
43
|
+
)
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
sig { params(file: SimpleCov::SourceFile).void }
|
|
33
47
|
def group_missed_lines(file)
|
|
34
48
|
file.missed_lines.each do |line|
|
|
35
|
-
|
|
36
|
-
node = @nodes.find { |n| line_num.between?(n.start_line, n.end_line) }
|
|
37
|
-
node_name = node ? node.name : "Line #{line_num}"
|
|
38
|
-
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
|
|
39
|
-
T.must(@node_deficits[node_name]).lines << line
|
|
49
|
+
add_missed_line(line)
|
|
40
50
|
end
|
|
41
51
|
end
|
|
42
52
|
|
|
53
|
+
sig { params(line: SimpleCov::SourceFile::Line).void }
|
|
54
|
+
def add_missed_line(line)
|
|
55
|
+
line_num = line.line_number
|
|
56
|
+
node = @nodes.reverse.find { |n| line_num.between?(n.start_line, n.end_line) }
|
|
57
|
+
node_name = node ? node.name : "Line #{line_num}"
|
|
58
|
+
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
|
|
59
|
+
T.must(@node_deficits[node_name]).lines << line
|
|
60
|
+
end
|
|
61
|
+
|
|
43
62
|
sig { params(file: SimpleCov::SourceFile).void }
|
|
44
63
|
def group_missed_branches(file)
|
|
45
64
|
return unless file.respond_to?(:branches) && file.branches.any?
|
|
@@ -53,7 +72,7 @@ module SimpleCov
|
|
|
53
72
|
def add_missed_branch(branch)
|
|
54
73
|
start_line = branch.start_line
|
|
55
74
|
end_line = branch.end_line
|
|
56
|
-
node = @nodes.find { |n| start_line >= n.start_line && end_line <= n.end_line }
|
|
75
|
+
node = @nodes.reverse.find { |n| start_line >= n.start_line && end_line <= n.end_line }
|
|
57
76
|
node_name = node ? node.name : "Lines #{start_line}-#{end_line}"
|
|
58
77
|
@node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
|
|
59
78
|
T.must(@node_deficits[node_name]).branches << branch
|
|
@@ -48,7 +48,7 @@ module SimpleCov
|
|
|
48
48
|
|
|
49
49
|
occurrences, current = count_snippet_occurrences(first_line_of_snippet, line_num, source_lines, node)
|
|
50
50
|
|
|
51
|
-
occurrences > 1 ? "(Occurrence #{current} of #{occurrences})
|
|
51
|
+
occurrences > 1 ? "(Occurrence #{current} of #{occurrences})." : ''
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
sig do
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative 'ast_resolver'
|
|
5
|
+
require 'time'
|
|
5
6
|
require_relative 'markdown_builder/snippet_formatter'
|
|
6
7
|
require_relative 'markdown_builder/bypass_compiler'
|
|
7
8
|
require_relative 'markdown_builder/deficit_grouper'
|
|
@@ -79,7 +80,7 @@ module SimpleCov
|
|
|
79
80
|
@buffer.puts "**Status:** #{status}"
|
|
80
81
|
@buffer.puts "**Global Line Coverage:** #{@result.covered_percent.round(1)}%"
|
|
81
82
|
@buffer.puts "**Global Branch Coverage:** #{calculate_branch_pct.round(1)}%"
|
|
82
|
-
@buffer.puts "**Generated At:** #{Time.now}"
|
|
83
|
+
@buffer.puts "**Generated At:** #{Time.now.iso8601} (Local Timezone)"
|
|
83
84
|
@buffer.puts ''
|
|
84
85
|
end
|
|
85
86
|
|
|
@@ -87,18 +88,22 @@ module SimpleCov
|
|
|
87
88
|
def calculate_branch_pct
|
|
88
89
|
return 0.0 unless @result.respond_to?(:covered_branches) && @result.respond_to?(:total_branches)
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
total = @result.total_branches
|
|
92
|
+
return 0.0 if total.to_i.zero?
|
|
93
|
+
|
|
94
|
+
covered = @result.covered_branches
|
|
95
|
+
covered.to_f / total * 100.0
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
# Appends a critical alert if the output hit the token-ceiling constraint and was forcibly terminated.
|
|
96
99
|
sig { void }
|
|
97
100
|
def write_truncation_warning
|
|
98
101
|
@buffer.puts '> **[WARNING] TRUNCATION NOTIFICATION:**'
|
|
99
|
-
msg =
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
+
msg = '> The total coverage deficit report exceeded the maximum token ' \
|
|
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.'
|
|
102
107
|
@buffer.puts msg
|
|
103
108
|
end
|
|
104
109
|
end
|
data/lib/simplecov-ai/version.rb
CHANGED
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vitalii Lazebnyi
|
|
@@ -336,7 +336,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
336
336
|
requirements:
|
|
337
337
|
- - ">="
|
|
338
338
|
- !ruby/object:Gem::Version
|
|
339
|
-
version:
|
|
339
|
+
version: 2.7.0
|
|
340
340
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
341
341
|
requirements:
|
|
342
342
|
- - ">="
|
metadata.gz.sig
CHANGED
|
Binary file
|