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.
@@ -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 - Modern color scheme */
425
- .tI { color: #475569; } /* identifier - slate-600 */
426
- .tD { color: #7c3aed; font-style: italic; } /* data type - violet-600 */
427
- .tK { color: #dc2626; font-weight: 500; } /* keyword - red-600 */
428
- .tC { color: #6366f1; font-style: italic; } /* comment - indigo-500 */
429
- .tQ { color: #059669; font-style: italic; } /* sql statement - emerald-600 */
430
- .tS { color: #10b981; } /* string literal - emerald-500 */
431
- .tL { color: #ea580c; font-style: italic; } /* label - orange-600 */
432
- .tM { color: var(--color-text-secondary); } /* dollar quote marker */
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-text);
463
+ color: var(--color-success);
464
464
  }
465
465
 
466
466
  /* LOOP COVERAGE */
467
- .l0000, .l0001, .l0010, .l0100, .l0011, .l0101, .l0110, .l0111,
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-text);
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: #f59e0b;
491
+ color: var(--color-warning);
491
492
  }
492
493
 
493
494
  .b11 {
494
- color: var(--color-text);
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("&", "&amp;")
83
+ .gsub("<", "&lt;")
84
+ .gsub(">", "&gt;")
85
+ .gsub("\"", "&quot;")
86
+ .gsub("'", "&apos;")
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
@@ -4,5 +4,7 @@ module Piggly
4
4
  autoload :Base, "piggly/reporter/base"
5
5
  autoload :Index, "piggly/reporter/index"
6
6
  autoload :Procedure, "piggly/reporter/procedure"
7
+ autoload :Sonar, "piggly/reporter/sonar"
8
+ autoload :SchemaCsv, "piggly/reporter/schema_csv"
7
9
  end
8
10
  end
data/lib/piggly/tags.rb CHANGED
@@ -175,7 +175,7 @@ module Piggly
175
175
  end
176
176
 
177
177
  def style
178
- "l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}}"
178
+ "l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}.join}"
179
179
  end
180
180
 
181
181
  def to_f
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
@@ -5,5 +5,6 @@ module Piggly
5
5
  autoload :Cacheable, "piggly/util/cacheable"
6
6
  autoload :Enumerable, "piggly/util/enumerable"
7
7
  autoload :File, "piggly/util/file"
8
+ autoload :LineNumbers, "piggly/util/line_numbers"
8
9
  end
9
10
  end
@@ -2,9 +2,9 @@ module Piggly
2
2
  module VERSION
3
3
  MAJOR = 2
4
4
  MINOR = 3
5
- TINY = 3
5
+ TINY = 5
6
6
 
7
- RELEASE_DATE = "2025-12-12"
7
+ RELEASE_DATE = "2026-05-21"
8
8
  end
9
9
 
10
10
  class << VERSION
@@ -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