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 +4 -4
- data/README.md +30 -4
- data/lib/piggly/command/report.rb +58 -6
- data/lib/piggly/compiler/coverage_report.rb +1 -1
- data/lib/piggly/compiler/line_coverage.rb +211 -0
- data/lib/piggly/compiler.rb +1 -0
- data/lib/piggly/parser/grammar.tt +760 -748
- data/lib/piggly/reporter/base.rb +17 -1
- data/lib/piggly/reporter/index.rb +43 -1
- data/lib/piggly/reporter/procedure.rb +11 -1
- data/lib/piggly/reporter/resources/piggly.css +21 -20
- data/lib/piggly/reporter/schema_csv.rb +98 -0
- data/lib/piggly/reporter/sonar.rb +99 -0
- data/lib/piggly/reporter.rb +2 -0
- data/lib/piggly/tags.rb +1 -1
- data/lib/piggly/task.rb +4 -1
- data/lib/piggly/util/line_numbers.rb +25 -0
- data/lib/piggly/util.rb +1 -0
- data/lib/piggly/version.rb +2 -2
- data/spec/examples/grammar/statements/loop_spec.rb +9 -0
- data/spec/examples/grammar/statements/sql_spec.rb +14 -0
- data/spec/examples/reporter/schema_csv_spec.rb +72 -0
- data/spec/examples/util/line_numbers_spec.rb +59 -0
- data/spec/issues/037_spec.rb +30 -0
- data/spec/issues/case_in_condition_spec.rb +50 -0
- data/spec/issues/commit_spec.rb +34 -0
- data/spec/spec_helper.rb +1 -1
- metadata +25 -2
data/lib/piggly/reporter/base.rb
CHANGED
|
@@ -31,13 +31,15 @@ module Piggly
|
|
|
31
31
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
|
-
def aggregate(label, summary)
|
|
34
|
+
def aggregate(label, summary, line_summary = nil)
|
|
35
35
|
tag :p, label, :class => "summary"
|
|
36
36
|
tag :table, :class => "summary" do
|
|
37
37
|
tag :tr do
|
|
38
|
+
tag :th, "Lines"
|
|
38
39
|
tag :th, "Blocks"
|
|
39
40
|
tag :th, "Loops"
|
|
40
41
|
tag :th, "Branches"
|
|
42
|
+
tag :th, "Line Coverage"
|
|
41
43
|
tag :th, "Block Coverage"
|
|
42
44
|
tag :th, "Loop Coverage"
|
|
43
45
|
tag :th, "Branch Coverage"
|
|
@@ -49,13 +51,27 @@ module Piggly
|
|
|
49
51
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
50
52
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
51
53
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
54
|
+
tag(:td, :class => "count") { tag :span, -1 }
|
|
55
|
+
tag(:td, :class => "pct") { tag :span, -1 }
|
|
52
56
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
53
57
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
54
58
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
55
59
|
else
|
|
60
|
+
# Line coverage (from LineCoverage module)
|
|
61
|
+
if line_summary
|
|
62
|
+
tag(:td, (line_summary[:count] || 0), :class => "count")
|
|
63
|
+
else
|
|
64
|
+
tag(:td, :class => "count") { tag :span, -1 }
|
|
65
|
+
end
|
|
56
66
|
tag(:td, (summary[:block][:count] || 0), :class => "count")
|
|
57
67
|
tag(:td, (summary[:loop][:count] || 0), :class => "count")
|
|
58
68
|
tag(:td, (summary[:branch][:count] || 0), :class => "count")
|
|
69
|
+
# Line coverage percentage
|
|
70
|
+
if line_summary
|
|
71
|
+
tag(:td, :class => "pct") { percent(line_summary[:percent]) }
|
|
72
|
+
else
|
|
73
|
+
tag(:td, :class => "pct") { tag :span, -1 }
|
|
74
|
+
end
|
|
59
75
|
tag(:td, :class => "pct") { percent(summary[:block][:percent]) }
|
|
60
76
|
tag(:td, :class => "pct") { percent(summary[:loop][:percent]) }
|
|
61
77
|
tag(:td, :class => "pct") { percent(summary[:branch][:percent]) }
|
|
@@ -5,11 +5,16 @@ module Piggly
|
|
|
5
5
|
|
|
6
6
|
def initialize(config, profile)
|
|
7
7
|
@config, @profile = config, profile
|
|
8
|
+
@line_coverage = Compiler::LineCoverage.new(config)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def report(procedures, index)
|
|
11
12
|
io = File.open("#{report_path}/index.html", "w")
|
|
12
13
|
|
|
14
|
+
# Calculate line coverage for all procedures
|
|
15
|
+
all_line_coverage = calculate_all_line_coverage(procedures)
|
|
16
|
+
overall_line_summary = @line_coverage.summary(all_line_coverage)
|
|
17
|
+
|
|
13
18
|
html(io) do
|
|
14
19
|
tag :html do
|
|
15
20
|
tag :head do
|
|
@@ -20,7 +25,7 @@ module Piggly
|
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
tag :body do
|
|
23
|
-
aggregate("PL/pgSQL Coverage Summary", @profile.summary)
|
|
28
|
+
aggregate("PL/pgSQL Coverage Summary", @profile.summary, overall_line_summary)
|
|
24
29
|
table(procedures.sort_by{|p| index.label(p) }, index)
|
|
25
30
|
timestamp
|
|
26
31
|
end
|
|
@@ -32,14 +37,38 @@ module Piggly
|
|
|
32
37
|
|
|
33
38
|
private
|
|
34
39
|
|
|
40
|
+
# Calculate combined line coverage summary for all procedures
|
|
41
|
+
# Returns aggregated coverage data that can be passed to @line_coverage.summary()
|
|
42
|
+
def calculate_all_line_coverage(procedures)
|
|
43
|
+
all_coverage = {}
|
|
44
|
+
next_key = 1 # Use sequential keys to avoid collisions
|
|
45
|
+
|
|
46
|
+
procedures.each do |procedure|
|
|
47
|
+
begin
|
|
48
|
+
coverage = @line_coverage.calculate(procedure, @profile)
|
|
49
|
+
# Add each line's coverage data with a unique key
|
|
50
|
+
# Keys don't need to represent actual line numbers for summary calculation
|
|
51
|
+
coverage.each do |_line, data|
|
|
52
|
+
all_coverage[next_key] = data
|
|
53
|
+
next_key += 1
|
|
54
|
+
end
|
|
55
|
+
rescue => e
|
|
56
|
+
$stderr.puts "Index: ERROR calculating coverage for #{procedure.name}: #{e.class}: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
all_coverage
|
|
60
|
+
end
|
|
61
|
+
|
|
35
62
|
def table(procedures, index)
|
|
36
63
|
tag :div, :class => "table-wrapper" do
|
|
37
64
|
tag :table, :class => "summary sortable" do
|
|
38
65
|
tag :tr do
|
|
39
66
|
tag :th, "Procedure"
|
|
67
|
+
tag :th, "Lines"
|
|
40
68
|
tag :th, "Blocks"
|
|
41
69
|
tag :th, "Loops"
|
|
42
70
|
tag :th, "Branches"
|
|
71
|
+
tag :th, "Line Coverage"
|
|
43
72
|
tag :th, "Block Coverage"
|
|
44
73
|
tag :th, "Loop Coverage"
|
|
45
74
|
tag :th, "Branch Coverage"
|
|
@@ -50,6 +79,15 @@ module Piggly
|
|
|
50
79
|
row = k.modulo(2) == 0 ? "even" : "odd"
|
|
51
80
|
label = index.label(procedure)
|
|
52
81
|
|
|
82
|
+
# Calculate line coverage for this procedure
|
|
83
|
+
line_summary = nil
|
|
84
|
+
begin
|
|
85
|
+
coverage = @line_coverage.calculate(procedure, @profile)
|
|
86
|
+
line_summary = @line_coverage.summary(coverage)
|
|
87
|
+
rescue => e
|
|
88
|
+
# Skip if can't calculate
|
|
89
|
+
end
|
|
90
|
+
|
|
53
91
|
tag :tr, :class => row do
|
|
54
92
|
unless summary.include?(:block) or summary.include?(:loop) or summary.include?(:branch)
|
|
55
93
|
# Parser couldn't parse this file
|
|
@@ -57,14 +95,18 @@ module Piggly
|
|
|
57
95
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
58
96
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
59
97
|
tag(:td, :class => "count") { tag :span, -1 }
|
|
98
|
+
tag(:td, :class => "count") { tag :span, -1 }
|
|
99
|
+
tag(:td, :class => "pct") { tag :span, -1 }
|
|
60
100
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
61
101
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
62
102
|
tag(:td, :class => "pct") { tag :span, -1 }
|
|
63
103
|
else
|
|
64
104
|
tag(:td, :class => "file") { tag :a, label, :href => procedure.identifier + ".html" }
|
|
105
|
+
tag :td, (line_summary ? line_summary[:count] : 0), :class => "count"
|
|
65
106
|
tag :td, (summary[:block][:count] || 0), :class => "count"
|
|
66
107
|
tag :td, (summary[:loop][:count] || 0), :class => "count"
|
|
67
108
|
tag :td, (summary[:branch][:count] || 0), :class => "count"
|
|
109
|
+
tag(:td, :class => "pct") { percent(line_summary ? line_summary[:percent] : nil) }
|
|
68
110
|
tag(:td, :class => "pct") { percent(summary[:block][:percent]) }
|
|
69
111
|
tag(:td, :class => "pct") { percent(summary[:loop][:percent]) }
|
|
70
112
|
tag(:td, :class => "pct") { percent(summary[:branch][:percent]) }
|
|
@@ -5,6 +5,7 @@ module Piggly
|
|
|
5
5
|
|
|
6
6
|
def initialize(config, profile)
|
|
7
7
|
@config, @profile = config, profile
|
|
8
|
+
@line_coverage = Compiler::LineCoverage.new(config)
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def report(procedure)
|
|
@@ -14,6 +15,15 @@ module Piggly
|
|
|
14
15
|
compiler = Compiler::CoverageReport.new(@config)
|
|
15
16
|
data = compiler.compile(procedure, @profile)
|
|
16
17
|
|
|
18
|
+
# Calculate line coverage for this procedure
|
|
19
|
+
line_summary = nil
|
|
20
|
+
begin
|
|
21
|
+
coverage = @line_coverage.calculate(procedure, @profile)
|
|
22
|
+
line_summary = @line_coverage.summary(coverage)
|
|
23
|
+
rescue => e
|
|
24
|
+
# Skip if can't calculate
|
|
25
|
+
end
|
|
26
|
+
|
|
17
27
|
html(io) do
|
|
18
28
|
tag :html, :xmlns => "http://www.w3.org/1999/xhtml" do
|
|
19
29
|
tag :head do
|
|
@@ -25,7 +35,7 @@ module Piggly
|
|
|
25
35
|
|
|
26
36
|
tag :body do
|
|
27
37
|
tag :div, :class => "header" do
|
|
28
|
-
aggregate(procedure.name, @profile.summary(procedure))
|
|
38
|
+
aggregate(procedure.name, @profile.summary(procedure), line_summary)
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
tag :div, :class => "container" do
|
|
@@ -421,15 +421,15 @@ table.full td.covered {
|
|
|
421
421
|
width: 100% !important;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
/* SYNTAX HIGHLIGHTING -
|
|
425
|
-
.tI { color:
|
|
426
|
-
.tD { color:
|
|
427
|
-
.tK { color:
|
|
428
|
-
.tC { color:
|
|
429
|
-
.tQ { color:
|
|
430
|
-
.tS { color:
|
|
431
|
-
.tL { color:
|
|
432
|
-
.tM { color:
|
|
424
|
+
/* SYNTAX HIGHLIGHTING - Inherit coverage colors, except comments */
|
|
425
|
+
.tI { color: inherit; } /* identifier */
|
|
426
|
+
.tD { color: inherit; font-style: italic; } /* data type */
|
|
427
|
+
.tK { color: inherit; font-weight: 500; } /* keyword */
|
|
428
|
+
.tC { color: var(--color-text); font-style: italic; } /* comment */
|
|
429
|
+
.tQ { color: inherit; font-style: italic; } /* sql statement */
|
|
430
|
+
.tS { color: inherit; } /* string literal */
|
|
431
|
+
.tL { color: inherit; font-style: italic; } /* label */
|
|
432
|
+
.tM { color: inherit; } /* dollar quote marker */
|
|
433
433
|
|
|
434
434
|
/* CODE BLOCKS */
|
|
435
435
|
.b {
|
|
@@ -460,18 +460,23 @@ table.full td.covered {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
.c1 {
|
|
463
|
-
color: var(--color-
|
|
463
|
+
color: var(--color-success);
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
/* LOOP COVERAGE */
|
|
467
|
-
.l0000
|
|
467
|
+
.l0000 {
|
|
468
|
+
font-weight: 600;
|
|
469
|
+
color: var(--color-danger); /* red - never evaluated */
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.l0001, .l0010, .l0100, .l0011, .l0101, .l0110, .l0111,
|
|
468
473
|
.l1000, .l1001, .l1010, .l1100, .l1011, .l1101, .l1110 {
|
|
469
474
|
font-weight: 600;
|
|
470
|
-
color: var(--color-warning);
|
|
475
|
+
color: var(--color-warning); /* orange - partial coverage */
|
|
471
476
|
}
|
|
472
477
|
|
|
473
478
|
.l1111 {
|
|
474
|
-
color: var(--color-
|
|
479
|
+
color: var(--color-success); /* green - full coverage */
|
|
475
480
|
}
|
|
476
481
|
|
|
477
482
|
/* BRANCH DECISIONS */
|
|
@@ -480,18 +485,14 @@ table.full td.covered {
|
|
|
480
485
|
color: var(--color-danger);
|
|
481
486
|
}
|
|
482
487
|
|
|
483
|
-
.b01
|
|
484
|
-
font-weight: 600;
|
|
485
|
-
color: #10b981;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
+
.b01,
|
|
488
489
|
.b10 {
|
|
489
490
|
font-weight: 600;
|
|
490
|
-
color:
|
|
491
|
+
color: var(--color-warning);
|
|
491
492
|
}
|
|
492
493
|
|
|
493
494
|
.b11 {
|
|
494
|
-
color: var(--color-
|
|
495
|
+
color: var(--color-success);
|
|
495
496
|
}
|
|
496
497
|
|
|
497
498
|
/* SCROLLBAR STYLING (for webkit browsers) */
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "csv"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Piggly
|
|
5
|
+
module Reporter
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# Generates schema-level coverage summary in CSV format.
|
|
9
|
+
#
|
|
10
|
+
class SchemaCsv < Base
|
|
11
|
+
HEADERS = ["No", "Schema Name", "Objects Count", "Covered Objects", "Line Coverage Percent"].freeze
|
|
12
|
+
NO_SCHEMA = "<no schema>".freeze
|
|
13
|
+
|
|
14
|
+
def initialize(config, profile, output_path = nil)
|
|
15
|
+
@config = config
|
|
16
|
+
@profile = profile
|
|
17
|
+
@output_path = output_path || File.join(@config.report_root, "coverage_by_schema.csv")
|
|
18
|
+
@line_coverage = Compiler::LineCoverage.new(config)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Generate CSV schema coverage report for all procedures
|
|
22
|
+
# @param procedures [Array<Dumper::ReifiedProcedure>] list of procedures
|
|
23
|
+
def report(procedures)
|
|
24
|
+
FileUtils.makedirs(File.dirname(@output_path))
|
|
25
|
+
aggregates = aggregate_by_schema(procedures)
|
|
26
|
+
|
|
27
|
+
CSV.open(@output_path, "wb:UTF-8") do |csv|
|
|
28
|
+
csv << HEADERS
|
|
29
|
+
|
|
30
|
+
aggregates.each_with_index do |row, index|
|
|
31
|
+
csv << [
|
|
32
|
+
index + 1,
|
|
33
|
+
row[:schema_name],
|
|
34
|
+
row[:objects_count],
|
|
35
|
+
row[:covered_objects],
|
|
36
|
+
format("%0.2f", row[:coverage_percent])
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@output_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def aggregate_by_schema(procedures)
|
|
47
|
+
grouped = Hash.new do |hash, key|
|
|
48
|
+
hash[key] = {
|
|
49
|
+
schema_name: key,
|
|
50
|
+
objects_count: 0,
|
|
51
|
+
covered_objects: 0,
|
|
52
|
+
coverage_values: []
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
procedures.each do |procedure|
|
|
57
|
+
schema_name = schema_label(procedure)
|
|
58
|
+
row = grouped[schema_name]
|
|
59
|
+
row[:objects_count] += 1
|
|
60
|
+
|
|
61
|
+
begin
|
|
62
|
+
percent = line_coverage_percent(procedure)
|
|
63
|
+
next if percent.nil?
|
|
64
|
+
|
|
65
|
+
row[:covered_objects] += 1 if percent > 0.0
|
|
66
|
+
row[:coverage_values] << percent
|
|
67
|
+
rescue => e
|
|
68
|
+
$stderr.puts "Warning: Could not calculate schema CSV coverage for #{procedure.name}: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
grouped.keys.sort.map do |schema_name|
|
|
73
|
+
row = grouped[schema_name]
|
|
74
|
+
values = row[:coverage_values]
|
|
75
|
+
average = values.empty? ? 0.0 : values.inject(0.0, :+) / values.size
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
schema_name: row[:schema_name],
|
|
79
|
+
objects_count: row[:objects_count],
|
|
80
|
+
covered_objects: row[:covered_objects],
|
|
81
|
+
coverage_percent: average
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def schema_label(procedure)
|
|
87
|
+
schema = procedure.name.schema.to_s.strip
|
|
88
|
+
schema.empty? ? NO_SCHEMA : schema
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def line_coverage_percent(procedure)
|
|
92
|
+
coverage = @line_coverage.calculate(procedure, @profile)
|
|
93
|
+
@line_coverage.summary(coverage)[:percent]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Reporter
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# Generates SonarQube generic test coverage XML format.
|
|
6
|
+
#
|
|
7
|
+
# Format specification:
|
|
8
|
+
# https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/test-coverage/generic-test-data/
|
|
9
|
+
#
|
|
10
|
+
class Sonar < Base
|
|
11
|
+
|
|
12
|
+
def initialize(config, profile, output_path = nil)
|
|
13
|
+
@config = config
|
|
14
|
+
@profile = profile
|
|
15
|
+
@output_path = output_path || File.join(@config.report_root, "sonar-coverage.xml")
|
|
16
|
+
@line_coverage = Compiler::LineCoverage.new(config)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate Sonar coverage report for all procedures
|
|
20
|
+
# @param procedures [Array<Dumper::ReifiedProcedure>] list of procedures
|
|
21
|
+
def report(procedures)
|
|
22
|
+
FileUtils.makedirs(File.dirname(@output_path))
|
|
23
|
+
|
|
24
|
+
File.open(@output_path, "w:UTF-8") do |io|
|
|
25
|
+
io.puts '<?xml version="1.0" encoding="UTF-8"?>'
|
|
26
|
+
io.puts '<coverage version="1">'
|
|
27
|
+
|
|
28
|
+
procedures.each do |procedure|
|
|
29
|
+
begin
|
|
30
|
+
write_procedure_coverage(io, procedure)
|
|
31
|
+
rescue => e
|
|
32
|
+
# Skip procedures that can't be processed
|
|
33
|
+
$stderr.puts "Warning: Could not generate Sonar coverage for #{procedure.name}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
io.puts '</coverage>'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@output_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Write coverage data for a single procedure
|
|
46
|
+
def write_procedure_coverage(io, procedure)
|
|
47
|
+
coverage = @line_coverage.calculate(procedure, @profile)
|
|
48
|
+
return if coverage.empty?
|
|
49
|
+
|
|
50
|
+
# Get the source file path and convert to relative path
|
|
51
|
+
absolute_path = procedure.source_path(@config)
|
|
52
|
+
source_path = make_relative_path(absolute_path)
|
|
53
|
+
|
|
54
|
+
io.puts " <file path=\"#{escape_xml(source_path)}\">"
|
|
55
|
+
|
|
56
|
+
source = procedure.source(@config)
|
|
57
|
+
max_line = Util::LineNumbers.count(source)
|
|
58
|
+
|
|
59
|
+
# Sort lines and output coverage data
|
|
60
|
+
coverage.keys.sort.each do |line|
|
|
61
|
+
next if line < 1 || line > max_line
|
|
62
|
+
|
|
63
|
+
line_data = coverage[line]
|
|
64
|
+
write_line_coverage(io, line, line_data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
io.puts " </file>"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Write coverage data for a single line
|
|
71
|
+
def write_line_coverage(io, line_number, line_data)
|
|
72
|
+
attrs = []
|
|
73
|
+
attrs << "lineNumber=\"#{line_number}\""
|
|
74
|
+
attrs << "covered=\"#{line_data[:covered]}\""
|
|
75
|
+
|
|
76
|
+
io.puts " <lineToCover #{attrs.join(' ')}/>"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Escape special XML characters
|
|
80
|
+
def escape_xml(text)
|
|
81
|
+
text.to_s
|
|
82
|
+
.gsub("&", "&")
|
|
83
|
+
.gsub("<", "<")
|
|
84
|
+
.gsub(">", ">")
|
|
85
|
+
.gsub("\"", """)
|
|
86
|
+
.gsub("'", "'")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Convert absolute path to relative path from project root for cross-platform compatibility
|
|
90
|
+
def make_relative_path(absolute_path)
|
|
91
|
+
project_root = File.dirname(File.dirname(@config.cache_root))
|
|
92
|
+
|
|
93
|
+
relative = absolute_path.sub(/^#{Regexp.escape(project_root)}[\/\\]?/, '')
|
|
94
|
+
relative.gsub('\\', '/')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/piggly/reporter.rb
CHANGED
data/lib/piggly/tags.rb
CHANGED
data/lib/piggly/task.rb
CHANGED
|
@@ -118,12 +118,14 @@ module Piggly
|
|
|
118
118
|
class ReportTask < AbstractTask
|
|
119
119
|
attr_accessor :report_root, # Where to store reports (default piggly/report)
|
|
120
120
|
:accumulate, # Accumulate coverage from the previous run (default false)
|
|
121
|
-
:trace_file
|
|
121
|
+
:trace_file,
|
|
122
|
+
:schema_csv_path
|
|
122
123
|
|
|
123
124
|
def initialize(name = :report)
|
|
124
125
|
@accumulate = false
|
|
125
126
|
@trace_file = nil
|
|
126
127
|
@report_root = nil
|
|
128
|
+
@schema_csv_path = nil
|
|
127
129
|
super(name)
|
|
128
130
|
end
|
|
129
131
|
|
|
@@ -141,6 +143,7 @@ module Piggly
|
|
|
141
143
|
opts.concat(["--trace-file", @trace_file])
|
|
142
144
|
opts.concat(["--cache-root", @cache_root]) if @cache_root
|
|
143
145
|
opts.concat(["--report-root", @report_root]) if @report_root
|
|
146
|
+
opts.concat(["--schema-csv-path", @schema_csv_path]) if @schema_csv_path
|
|
144
147
|
|
|
145
148
|
case @procedures
|
|
146
149
|
when String then opts.concat(["--name", @procedures])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Util
|
|
3
|
+
|
|
4
|
+
module LineNumbers
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def count(source)
|
|
8
|
+
return 0 if source.nil? || source.empty?
|
|
9
|
+
|
|
10
|
+
source.lines.count
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# 1-based line number for a byte offset, clamped to [1, count(source)].
|
|
14
|
+
def at_offset(source, offset)
|
|
15
|
+
return 1 if source.nil? || source.empty?
|
|
16
|
+
|
|
17
|
+
offset = [[offset, 0].max, source.length].min
|
|
18
|
+
line = source[0...offset].count("\n") + 1
|
|
19
|
+
max_line = count(source)
|
|
20
|
+
[[line, 1].max, max_line].min
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/piggly/util.rb
CHANGED
data/lib/piggly/version.rb
CHANGED
|
@@ -23,6 +23,15 @@ module Piggly
|
|
|
23
23
|
cond.source_text.should == 'SELECT * FROM table '
|
|
24
24
|
cond.should be_sql
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
it "can loop over parenthesized query without whitespace after IN" do
|
|
28
|
+
node = parse(:stmtForLoop, 'FOR x IN(SELECT * FROM table) LOOP a := x; END LOOP;')
|
|
29
|
+
node.should be_statement
|
|
30
|
+
|
|
31
|
+
cond = node.find{|e| e.named?(:cond) }
|
|
32
|
+
cond.source_text.should == '(SELECT * FROM table) '
|
|
33
|
+
cond.should be_expression
|
|
34
|
+
end
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
describe "while loops" do
|
|
@@ -13,6 +13,20 @@ module Piggly
|
|
|
13
13
|
rest.should == ''
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
it "parses MERGE statements" do
|
|
17
|
+
node, rest = parse_some(:statement, <<-SQL.strip)
|
|
18
|
+
MERGE INTO users u
|
|
19
|
+
USING (SELECT 1 AS id) s
|
|
20
|
+
ON s.id = u.id
|
|
21
|
+
WHEN NOT MATCHED THEN
|
|
22
|
+
INSERT (id) VALUES (s.id);
|
|
23
|
+
SQL
|
|
24
|
+
node.should be_statement
|
|
25
|
+
node.count{|e| e.sql? }.should == 1
|
|
26
|
+
node.find{|e| e.sql? }.source_text.should =~ /\Amerge into users/mi
|
|
27
|
+
rest.should == ''
|
|
28
|
+
end
|
|
29
|
+
|
|
16
30
|
it "must end with a semicolon" do
|
|
17
31
|
expect{ parse(:statement, 'SELECT id FROM users') }.to raise_error(Piggly::Parser::Failure)
|
|
18
32
|
expect{ parse_some(:statement, 'SELECT id FROM users') }.to raise_error(Piggly::Parser::Failure)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "csv"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Piggly
|
|
7
|
+
|
|
8
|
+
describe Reporter::SchemaCsv do
|
|
9
|
+
let(:profile) { double(:profile) }
|
|
10
|
+
let(:line_coverage) { double(:line_coverage) }
|
|
11
|
+
let(:tmpdir) { Dir.mktmpdir("piggly-schema-csv") }
|
|
12
|
+
let(:config) { double(:config, :report_root => tmpdir) }
|
|
13
|
+
let(:output_path) { File.join(tmpdir, "schema-coverage.csv") }
|
|
14
|
+
|
|
15
|
+
def procedure(schema, name)
|
|
16
|
+
qualified_name = Dumper::QualifiedName.new(schema, name)
|
|
17
|
+
double(:procedure, :name => qualified_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
before do
|
|
21
|
+
allow(Compiler::LineCoverage).to receive(:new).with(config).and_return(line_coverage)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
after do
|
|
25
|
+
FileUtils.remove_entry(tmpdir) if File.exist?(tmpdir)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "writes schema coverage CSV with headers and aggregated rows" do
|
|
29
|
+
first_public = procedure("public", "alpha")
|
|
30
|
+
second_public = procedure("public", "beta")
|
|
31
|
+
app_proc = procedure("app", "gamma")
|
|
32
|
+
no_schema_proc = procedure(nil, "delta")
|
|
33
|
+
|
|
34
|
+
allow(line_coverage).to receive(:calculate).with(first_public, profile).and_return(:cov_public_1)
|
|
35
|
+
allow(line_coverage).to receive(:calculate).with(second_public, profile).and_return(:cov_public_2)
|
|
36
|
+
allow(line_coverage).to receive(:calculate).with(app_proc, profile).and_return(:cov_app)
|
|
37
|
+
allow(line_coverage).to receive(:calculate).with(no_schema_proc, profile).and_return(:cov_no_schema)
|
|
38
|
+
|
|
39
|
+
allow(line_coverage).to receive(:summary).with(:cov_public_1).and_return(:percent => 100.0)
|
|
40
|
+
allow(line_coverage).to receive(:summary).with(:cov_public_2).and_return(:percent => 0.0)
|
|
41
|
+
allow(line_coverage).to receive(:summary).with(:cov_app).and_return(:percent => nil)
|
|
42
|
+
allow(line_coverage).to receive(:summary).with(:cov_no_schema).and_return(:percent => 75.0)
|
|
43
|
+
|
|
44
|
+
reporter = Reporter::SchemaCsv.new(config, profile, output_path)
|
|
45
|
+
result_path = reporter.report([first_public, second_public, app_proc, no_schema_proc])
|
|
46
|
+
|
|
47
|
+
expect(result_path).to eq(output_path)
|
|
48
|
+
expect(File).to exist(output_path)
|
|
49
|
+
|
|
50
|
+
rows = CSV.read(output_path, :headers => true)
|
|
51
|
+
|
|
52
|
+
expect(rows.headers).to eq(["No", "Schema Name", "Objects Count", "Covered Objects", "Line Coverage Percent"])
|
|
53
|
+
|
|
54
|
+
by_schema = rows.each_with_object({}) do |row, hash|
|
|
55
|
+
hash[row["Schema Name"]] = row
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
expect(by_schema.fetch("public")["Objects Count"]).to eq("2")
|
|
59
|
+
expect(by_schema.fetch("public")["Covered Objects"]).to eq("1")
|
|
60
|
+
expect(by_schema.fetch("public")["Line Coverage Percent"]).to eq("50.00")
|
|
61
|
+
|
|
62
|
+
expect(by_schema.fetch("app")["Objects Count"]).to eq("1")
|
|
63
|
+
expect(by_schema.fetch("app")["Covered Objects"]).to eq("0")
|
|
64
|
+
expect(by_schema.fetch("app")["Line Coverage Percent"]).to eq("0.00")
|
|
65
|
+
|
|
66
|
+
expect(by_schema.fetch("<no schema>")["Objects Count"]).to eq("1")
|
|
67
|
+
expect(by_schema.fetch("<no schema>")["Covered Objects"]).to eq("1")
|
|
68
|
+
expect(by_schema.fetch("<no schema>")["Line Coverage Percent"]).to eq("75.00")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|