piggly-nsd 2.3.3 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b53364d89c087bf4c2ce49ec451ece2cb1c17694bab9e765e034a05493ef3999
4
- data.tar.gz: a46eea287041eeaa4130b730dd409f1f35663e6fca5258e96c639b1f567eeb11
3
+ metadata.gz: 736495bc6e9ad8785b755afb2b3a834cd0dff2e62983b0397e4d6fdf6f0fe3af
4
+ data.tar.gz: 958489e0a1a542230ad57dc8748ef4a231e9c03cea1452c34246887e9789746f
5
5
  SHA512:
6
- metadata.gz: 1cfd022ed8e03c56d372911943bd7b662a6ff967f63bde7b09a01e010b5117bc41ad0f5c245a30e72139776a5eece380ab9d2d60175462555a874cfb008e73c8
7
- data.tar.gz: b8653bb06b92d31eee24e5c9a317fe912dc61e01d46fae649dadeb4610b831f4ad50199683996cddc756727124be68514fc0d919b372d80f70347851539d7751
6
+ metadata.gz: ba81b385db041ea25c62a0777838d84ed89a165e489d6d4ef3f60ea908902acad3ba2a186e3eb7325a7520f407eead2ae3e6121fd8f4429664a3a9f00a848b01
7
+ data.tar.gz: e076e78ce56f6e8e7ed238f7fa354dfe9799b2707520394e5635162f8029359c581bf5d055b53efd0c0739b58410c59ee5cb9e94c08b7dd600ecf64f478fb7d4
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![CI](https://github.com/sergeiboikov/piggly/actions/workflows/ci.yml/badge.svg)](https://github.com/sergeiboikov/piggly/actions/workflows/ci.yml)
1
+ [![CI](https://github.com/NSDDeveloper/piggly/actions/workflows/ci.yml/badge.svg)](https://github.com/NSDDeveloper/piggly/actions/workflows/ci.yml)
2
2
  # Piggly-NSD
3
3
 
4
4
  Code coverage reports for PostgreSQL PL/pgSQL stored procedures
@@ -51,12 +51,16 @@ these events and generates prettified source code that is annotated with coverag
51
51
  This fork continues the version numbering from the original piggly project:
52
52
  - **2.3.1** - Last version of original piggly by Kyle Putnam
53
53
  - **2.3.2** - NSD fork with UTF-8 encoding support and updated dependencies
54
+ - **2.3.3** - Fixed issues
55
+ - **2.3.4** - Added SonarQube integration and line coverage metrics:
56
+ - New `--sonar-report-path PATH` option to generate SonarQube generic coverage XML
57
+ - Added LINES and LINE COVERAGE columns to HTML reports
54
58
 
55
59
  ## How to Install
56
60
 
57
61
  To install the latest from github:
58
62
 
59
- $ git clone https://github.com/sergeiboikov/piggly.git
63
+ $ git clone https://github.com/NSDDeveloper/piggly.git
60
64
  $ cd piggly
61
65
  $ bundle install
62
66
  $ bundle exec rake spec
@@ -122,11 +126,33 @@ might run:
122
126
  etc.
123
127
 
124
128
  To build the coverage report, have piggly read that file in by executing `piggly report < messages.txt`,
125
- or `piggly report -f messages.txt`. You don't actually need the intermediate file, you can pipe your
129
+ or `piggly report -f messages.txt`. You don't actually need the intermediate file, you can pipe your
126
130
  test suite directly in like `ant test 2>&1 | piggly report`.
127
131
 
128
132
  Once the report is built you can open it in `piggly/reports/index.html`.
129
133
 
134
+ ### SonarQube Integration
135
+
136
+ To generate a SonarQube-compatible coverage report, use the `--sonar-report-path` (or `-x`) option:
137
+
138
+ $ piggly report -f messages.txt --sonar-report-path piggly/sonar/coverage.xml
139
+
140
+ This generates an XML file in SonarQube's [generic test coverage format](https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/generic-test-data/).
141
+
142
+ ### Schema CSV Report
143
+
144
+ To generate a schema-level CSV report, use the `--schema-csv-path` option:
145
+
146
+ $ piggly report -f messages.txt --schema-csv-path piggly/reports/coverage_by_schema.csv
147
+
148
+ The CSV report groups procedures by schema and contains the following columns:
149
+
150
+ - `No`
151
+ - `Schema Name`
152
+ - `Objects Count`
153
+ - `Covered Objects`
154
+ - `Line Coverage Percent`
155
+
130
156
  ## Running the Examples
131
157
 
132
158
  $ cd piggly
@@ -167,4 +193,4 @@ Once the report is built you can open it in `piggly/reports/index.html`.
167
193
 
168
194
  ## Bugs & Issues
169
195
 
170
- Please report any issues or feature requests on the [github tracker](https://github.com/sergeiboikov/piggly/issues).
196
+ Please report any issues or feature requests on the [github tracker](https://github.com/NSDDeveloper/piggly/issues).
@@ -13,7 +13,7 @@ module Piggly
13
13
  class << Report
14
14
  def main(argv)
15
15
  require "pp"
16
- io, config = configure(argv)
16
+ io, config, sonar_path, schema_csv_path, html_report_requested = configure(argv)
17
17
 
18
18
  profile = Profile.new
19
19
  index = Dumper::Index.new(config)
@@ -33,12 +33,28 @@ module Piggly
33
33
 
34
34
  profile_procedures(config, procedures, profile)
35
35
  clear_coverage(config, profile)
36
-
37
36
  read_profile(config, io, profile)
38
37
  store_coverage(profile)
39
38
 
40
- create_index(config, index, procedures, profile)
41
- create_reports(config, procedures, profile)
39
+ # Generate HTML coverage report if requested
40
+ if html_report_requested
41
+ create_index(config, index, procedures, profile)
42
+ create_reports(config, procedures, profile)
43
+ end
44
+
45
+ # Generate Sonar coverage report if requested
46
+ if sonar_path
47
+ create_sonar_report(config, procedures, profile, sonar_path)
48
+ end
49
+
50
+ # Generate schema-level CSV report if requested
51
+ if schema_csv_path
52
+ create_schema_csv_report(config, procedures, profile, schema_csv_path)
53
+ end
54
+
55
+ unless html_report_requested || sonar_path || schema_csv_path
56
+ puts "Warning: No report output specified. Use -o for HTML reports, -x for Sonar report, or --schema-csv-path for CSV report."
57
+ end
42
58
  end
43
59
 
44
60
  # Adds the given procedures to Profile
@@ -118,14 +134,45 @@ module Piggly
118
134
  queue.execute
119
135
  end
120
136
 
137
+ # Create Sonar coverage XML report
138
+ #
139
+ def create_sonar_report(config, procedures, profile, output_path)
140
+ puts "creating Sonar coverage report"
141
+ reporter = Reporter::Sonar.new(config, profile, output_path)
142
+ path = reporter.report(procedures)
143
+ puts "Sonar coverage report written to: #{path}"
144
+ end
145
+
146
+ # Create schema-level CSV coverage report
147
+ #
148
+ def create_schema_csv_report(config, procedures, profile, output_path)
149
+ puts "creating schema CSV coverage report"
150
+ reporter = Reporter::SchemaCsv.new(config, profile, output_path)
151
+ path = reporter.report(procedures)
152
+ puts "Schema CSV coverage report written to: #{path}"
153
+ end
154
+
121
155
  def configure(argv, config = Config.new)
122
156
  io = $stdin
157
+ sonar_path = nil
158
+ schema_csv_path = nil
159
+ html_report_requested = false
160
+
123
161
  p = OptionParser.new do |o|
124
162
  o.on("-t", "--dry-run", "only print the names of matching procedures", &o_dry_run(config))
125
163
  o.on("-s", "--select PATTERN", "select procedures matching PATTERN", &o_select(config))
126
164
  o.on("-r", "--reject PATTERN", "ignore procedures matching PATTERN", &o_reject(config))
127
165
  o.on("-c", "--cache-root PATH", "local cache directory", &o_cache_root(config))
128
- o.on("-o", "--report-root PATH", "report output directory", &o_report_root(config))
166
+ o.on("-o", "--report-root PATH", "report output directory") do |path|
167
+ config.report_root = path
168
+ html_report_requested = true
169
+ end
170
+ o.on("-x", "--sonar-report-path PATH", "generate Sonar coverage XML report at PATH") do |path|
171
+ sonar_path = path
172
+ end
173
+ o.on("--schema-csv-path PATH", "generate schema-level CSV coverage report at PATH") do |path|
174
+ schema_csv_path = path
175
+ end
129
176
  o.on("-a", "--accumulate", "accumulate data from the previous run", &o_accumulate(config))
130
177
  o.on("-V", "--version", "show version", &o_version(config))
131
178
  o.on("-h", "--help", "show this message") { abort o.to_s }
@@ -140,13 +187,18 @@ module Piggly
140
187
 
141
188
  begin
142
189
  p.parse! argv
190
+
191
+ unless html_report_requested || sonar_path || schema_csv_path
192
+ raise OptionParser::MissingArgument,
193
+ "at least one report type required: use -o for HTML reports, -x for Sonar report, or --schema-csv-path for CSV report"
194
+ end
143
195
 
144
196
  if io.eql?($stdin) and $stdin.tty?
145
197
  raise OptionParser::MissingArgument,
146
198
  "stdin must be a pipe, or use --input PATH"
147
199
  end
148
200
 
149
- return io, config
201
+ return io, config, sonar_path, schema_csv_path, html_report_requested
150
202
  rescue OptionParser::InvalidOption,
151
203
  OptionParser::InvalidArgument,
152
204
  OptionParser::MissingArgument
@@ -23,7 +23,7 @@ module Piggly
23
23
  data = trace.compile(procedure)
24
24
 
25
25
  return :html => traverse(data[:tree], profile),
26
- :lines => 1 .. procedure.source(@config).count("\n") + 1
26
+ :lines => 1 .. Util::LineNumbers.count(procedure.source(@config))
27
27
  end
28
28
 
29
29
  protected
@@ -0,0 +1,211 @@
1
+ module Piggly
2
+ module Compiler
3
+
4
+ #
5
+ # Calculates line-level coverage from tagged parse tree nodes.
6
+ #
7
+ class LineCoverage
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ # Calculate line coverage for a procedure
13
+ # @param procedure [Dumper::ReifiedProcedure, Dumper::SkeletonProcedure]
14
+ # @param profile [Profile]
15
+ # @return [Hash] { line_number => { covered: bool } }
16
+ def calculate(procedure, profile)
17
+ Parser.parser
18
+
19
+ compiler = TraceCompiler.new(@config)
20
+
21
+ # Let compile() handle staleness - it will recompile if needed
22
+ data = compiler.compile(procedure)
23
+
24
+ # Return empty if compilation failed (tree is nil)
25
+ tree = data[:tree]
26
+ return {} if tree.nil?
27
+
28
+ source = procedure.source(@config)
29
+
30
+ coverage = {}
31
+ traverse(tree, profile, source, coverage)
32
+ coverage
33
+ end
34
+
35
+ # Calculate summary statistics from coverage data
36
+ # @param coverage [Hash] line coverage data from calculate()
37
+ # @return [Hash] { count: Integer, percent: Float }
38
+ def summary(coverage)
39
+ return { count: 0, percent: nil } if coverage.empty?
40
+
41
+ total_lines = coverage.size
42
+ covered_lines = coverage.count { |_, v| v[:covered] }
43
+
44
+ {
45
+ count: total_lines,
46
+ percent: total_lines > 0 ? (covered_lines.to_f / total_lines * 100) : nil
47
+ }
48
+ end
49
+
50
+
51
+ protected
52
+
53
+ # Traverse the parse tree and collect coverage data for each line
54
+ # @param node [NodeClass] parse tree node
55
+ # @param profile [Profile] coverage profile with tags
56
+ # @param source [String] source code text
57
+ # @param coverage [Hash] accumulator for line coverage data
58
+ def traverse(node, profile, source, coverage)
59
+ if node.tagged?
60
+ begin
61
+ tag = profile[node.tag_id]
62
+
63
+ # Get line numbers for this node
64
+ start_line, end_line = node_line_range(node, source)
65
+
66
+ # Record coverage for each line spanned by this node
67
+ # Skip lines containing only structural keywords (begin, end, declare, etc.)
68
+ if start_line && end_line && start_line > 0 && end_line >= start_line
69
+ (start_line..end_line).each do |line|
70
+ next if excluded_line?(source, line)
71
+ record_line_coverage(coverage, line, tag)
72
+ end
73
+ end
74
+ rescue RuntimeError => e
75
+ # Skip nodes where tag lookup fails (expected for some nodes)
76
+ rescue => e
77
+ # Skip nodes with unexpected errors
78
+ end
79
+ end
80
+
81
+ # Recurse into child nodes
82
+ if node.respond_to?(:elements) && node.elements
83
+ node.elements.each { |child| traverse(child, profile, source, coverage) }
84
+ end
85
+
86
+ coverage
87
+ end
88
+
89
+ # Check if a source line should be excluded from coverage
90
+ # Lines containing only structural keywords or comments are not executable
91
+ # @param source [String] full source code
92
+ # @param line_number [Integer] 1-based line number
93
+ # @return [Boolean] true if line should be excluded
94
+ def excluded_line?(source, line_number)
95
+ max_line = Util::LineNumbers.count(source)
96
+ return true if line_number < 1 || line_number > max_line
97
+
98
+ line_content = source.lines[line_number - 1].strip.downcase
99
+
100
+ # Exclude lines containing only structural keywords or comments
101
+ excluded_patterns = [
102
+ /\A\s*end\s*;\s*\z/i, # end;
103
+ /\A\s*begin\s*\z/i, # begin
104
+ /\A\s*declare\s*\z/i, # declare
105
+ /\A\s*\$\$\s*\z/, # $$ (dollar quoting)
106
+ /\A\s*\z/, # empty lines
107
+ /\A\s*--/, # single-line comments (-- ...)
108
+ /\A\s*\/\*.*\*\/\s*\z/ # single-line block comments (/* ... */)
109
+ ]
110
+
111
+ excluded_patterns.any? { |pattern| line_content =~ pattern }
112
+ end
113
+
114
+ # Calculate the line range for a node
115
+ # @param node [NodeClass] parse tree node
116
+ # @param source [String] source code text
117
+ # @return [Array<Integer, Integer>] start_line and end_line, or [nil, nil] if unable to calculate
118
+ def node_line_range(node, source)
119
+ return [nil, nil] unless node.respond_to?(:interval)
120
+ return [nil, nil] unless source.is_a?(String) && !source.empty?
121
+
122
+ interval = node.interval
123
+ return [nil, nil] unless interval.is_a?(Range)
124
+
125
+ start_pos = interval.first.to_i
126
+ return [nil, nil] if start_pos < 0
127
+
128
+ # Handle exclusive ranges (most common in Treetop)
129
+ if interval.exclude_end?
130
+ end_pos = [interval.end.to_i - 1, start_pos].max
131
+ else
132
+ end_pos = interval.end.to_i
133
+ end
134
+
135
+ # Clamp to source bounds
136
+ source_len = source.length
137
+ start_pos = [start_pos, source_len - 1].min if source_len > 0
138
+ end_pos = [end_pos, source_len - 1].min if source_len > 0
139
+
140
+ start_line = Util::LineNumbers.at_offset(source, start_pos)
141
+ end_line = Util::LineNumbers.at_offset(source, end_pos + 1)
142
+
143
+ [start_line, end_line]
144
+ end
145
+
146
+ # Record coverage data for a specific line
147
+ # @param coverage [Hash] accumulator
148
+ # @param line [Integer] line number
149
+ # @param tag [Tags::AbstractTag] the tag for this node
150
+ def record_line_coverage(coverage, line, tag)
151
+ coverage[line] ||= {
152
+ covered: nil, # nil = no block/loop tags yet, will be determined by branches if any
153
+ has_block_or_loop: false
154
+ }
155
+
156
+ case tag.type
157
+ when :block
158
+ # Block tags affect line coverage - line is covered if block was executed
159
+ coverage[line][:has_block_or_loop] = true
160
+ if coverage[line][:covered].nil?
161
+ coverage[line][:covered] = tag.complete?
162
+ else
163
+ coverage[line][:covered] = coverage[line][:covered] && tag.complete?
164
+ end
165
+
166
+ when :loop
167
+ # A loop is covered if it was executed at least once (any iteration pattern)
168
+ coverage[line][:has_block_or_loop] = true
169
+ loop_executed = loop_was_executed?(tag)
170
+ if coverage[line][:covered].nil?
171
+ coverage[line][:covered] = loop_executed
172
+ else
173
+ coverage[line][:covered] = coverage[line][:covered] && loop_executed
174
+ end
175
+
176
+ when :branch
177
+ # For lines with only branches, mark as covered if at least one branch was taken
178
+ unless coverage[line][:has_block_or_loop]
179
+ if tag.is_a?(Tags::ConditionalBranchTag)
180
+ branch_taken = tag.true || tag.false
181
+ if coverage[line][:covered].nil?
182
+ coverage[line][:covered] = branch_taken
183
+ else
184
+ coverage[line][:covered] = coverage[line][:covered] || branch_taken
185
+ end
186
+ else
187
+ # Unconditional branches (return, exit, etc.)
188
+ if coverage[line][:covered].nil?
189
+ coverage[line][:covered] = tag.complete?
190
+ else
191
+ coverage[line][:covered] = coverage[line][:covered] || tag.complete?
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ # Ensure covered has a boolean value (default to false if still nil)
198
+ coverage[line][:covered] = false if coverage[line][:covered].nil?
199
+ end
200
+
201
+ # Check if a loop tag indicates the loop was executed at least once
202
+ # For Sonar, partial loop coverage counts as covered
203
+ # @param tag [Tags::AbstractLoopTag] the loop tag
204
+ # @return [Boolean] true if loop was executed (any iteration pattern)
205
+ def loop_was_executed?(tag)
206
+ tag.pass || tag.once || tag.twice || tag.ends
207
+ end
208
+ end
209
+
210
+ end
211
+ end
@@ -3,5 +3,6 @@ module Piggly
3
3
  autoload :CacheDir, "piggly/compiler/cache_dir"
4
4
  autoload :TraceCompiler, "piggly/compiler/trace_compiler"
5
5
  autoload :CoverageReport, "piggly/compiler/coverage_report"
6
+ autoload :LineCoverage, "piggly/compiler/line_coverage"
6
7
  end
7
8
  end