piggly 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +163 -0
- data/Rakefile +29 -15
- data/bin/piggly +4 -244
- data/lib/piggly.rb +19 -17
- data/lib/piggly/command.rb +9 -0
- data/lib/piggly/command/base.rb +148 -0
- data/lib/piggly/command/report.rb +162 -0
- data/lib/piggly/command/test.rb +157 -0
- data/lib/piggly/command/trace.rb +90 -0
- data/lib/piggly/command/untrace.rb +78 -0
- data/lib/piggly/compiler.rb +7 -5
- data/lib/piggly/compiler/cache_dir.rb +119 -0
- data/lib/piggly/compiler/coverage_report.rb +63 -0
- data/lib/piggly/compiler/trace_compiler.rb +105 -0
- data/lib/piggly/config.rb +47 -22
- data/lib/piggly/dumper.rb +9 -0
- data/lib/piggly/dumper/index.rb +121 -0
- data/lib/piggly/dumper/qualified_name.rb +36 -0
- data/lib/piggly/dumper/qualified_type.rb +81 -0
- data/lib/piggly/dumper/reified_procedure.rb +142 -0
- data/lib/piggly/dumper/skeleton_procedure.rb +102 -0
- data/lib/piggly/installer.rb +84 -42
- data/lib/piggly/parser.rb +43 -49
- data/lib/piggly/parser/grammar.tt +289 -313
- data/lib/piggly/parser/nodes.rb +270 -211
- data/lib/piggly/parser/traversal.rb +35 -33
- data/lib/piggly/parser/treetop_ruby19_patch.rb +1 -1
- data/lib/piggly/profile.rb +81 -60
- data/lib/piggly/reporter.rb +5 -18
- data/lib/piggly/reporter/base.rb +103 -0
- data/lib/piggly/reporter/html_dsl.rb +63 -0
- data/lib/piggly/reporter/index.rb +108 -0
- data/lib/piggly/reporter/procedure.rb +104 -0
- data/lib/piggly/reporter/resources/highlight.js +21 -0
- data/lib/piggly/reporter/{piggly.css → resources/piggly.css} +52 -12
- data/lib/piggly/reporter/{sortable.js → resources/sortable.js} +0 -0
- data/lib/piggly/tags.rb +280 -0
- data/lib/piggly/task.rb +191 -40
- data/lib/piggly/util.rb +8 -27
- data/lib/piggly/util/blankslate.rb +114 -0
- data/lib/piggly/util/cacheable.rb +19 -0
- data/lib/piggly/util/enumerable.rb +44 -0
- data/lib/piggly/util/file.rb +17 -0
- data/lib/piggly/util/process_queue.rb +96 -0
- data/lib/piggly/util/thunk.rb +39 -0
- data/lib/piggly/version.rb +8 -8
- data/spec/examples/compiler/cacheable_spec.rb +190 -0
- data/spec/examples/compiler/report_spec.rb +25 -0
- data/spec/{compiler → examples/compiler}/trace_spec.rb +7 -57
- data/spec/examples/config_spec.rb +61 -0
- data/spec/examples/dumper/index_spec.rb +197 -0
- data/spec/examples/dumper/procedure_spec.rb +116 -0
- data/spec/{grammar → examples/grammar}/expression_spec.rb +60 -60
- data/spec/{grammar → examples/grammar}/statements/assignment_spec.rb +15 -15
- data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
- data/spec/{grammar → examples/grammar}/statements/exception_spec.rb +10 -10
- data/spec/{grammar → examples/grammar}/statements/if_spec.rb +47 -34
- data/spec/{grammar → examples/grammar}/statements/loop_spec.rb +5 -5
- data/spec/{grammar → examples/grammar}/statements/sql_spec.rb +11 -11
- data/spec/{grammar → examples/grammar}/tokens/comment_spec.rb +11 -11
- data/spec/{grammar → examples/grammar}/tokens/datatype_spec.rb +14 -8
- data/spec/{grammar → examples/grammar}/tokens/identifier_spec.rb +26 -10
- data/spec/{grammar → examples/grammar}/tokens/keyword_spec.rb +5 -5
- data/spec/{grammar → examples/grammar}/tokens/label_spec.rb +7 -7
- data/spec/{grammar → examples/grammar}/tokens/literal_spec.rb +1 -1
- data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
- data/spec/{grammar → examples/grammar}/tokens/number_spec.rb +1 -1
- data/spec/{grammar → examples/grammar}/tokens/sqlkeywords_spec.rb +1 -1
- data/spec/{grammar → examples/grammar}/tokens/string_spec.rb +9 -9
- data/spec/{grammar → examples/grammar}/tokens/whitespace_spec.rb +1 -1
- data/spec/examples/installer_spec.rb +59 -0
- data/spec/examples/parser/nodes_spec.rb +73 -0
- data/spec/examples/parser/traversal_spec.rb +14 -0
- data/spec/examples/parser_spec.rb +115 -0
- data/spec/examples/profile_spec.rb +153 -0
- data/spec/{reporter/html_spec.rb → examples/reporter/html/dsl_spec.rb} +0 -0
- data/spec/examples/reporter/html/index_spec.rb +0 -0
- data/spec/examples/reporter/html_spec.rb +1 -0
- data/spec/examples/reporter_spec.rb +0 -0
- data/spec/{compiler → examples}/tags_spec.rb +10 -10
- data/spec/examples/task_spec.rb +0 -0
- data/spec/examples/util/cacheable_spec.rb +41 -0
- data/spec/examples/util/enumerable_spec.rb +64 -0
- data/spec/examples/util/file_spec.rb +40 -0
- data/spec/examples/util/process_queue_spec.rb +16 -0
- data/spec/examples/util/thunk_spec.rb +58 -0
- data/spec/examples/version_spec.rb +0 -0
- data/spec/issues/007_spec.rb +25 -0
- data/spec/issues/008_spec.rb +73 -0
- data/spec/issues/018_spec.rb +25 -0
- data/spec/spec_helper.rb +253 -9
- metadata +136 -93
- data/README.markdown +0 -116
- data/lib/piggly/compiler/cache.rb +0 -151
- data/lib/piggly/compiler/pretty.rb +0 -67
- data/lib/piggly/compiler/queue.rb +0 -46
- data/lib/piggly/compiler/tags.rb +0 -244
- data/lib/piggly/compiler/trace.rb +0 -91
- data/lib/piggly/filecache.rb +0 -40
- data/lib/piggly/parser/parser.rb +0 -11794
- data/lib/piggly/reporter/html.rb +0 -207
- data/spec/compiler/cache_spec.rb +0 -9
- data/spec/compiler/pretty_spec.rb +0 -9
- data/spec/compiler/queue_spec.rb +0 -3
- data/spec/compiler/rewrite_spec.rb +0 -3
- data/spec/config_spec.rb +0 -58
- data/spec/filecache_spec.rb +0 -70
- data/spec/fixtures/snippets.sql +0 -158
- data/spec/grammar/tokens/lval_spec.rb +0 -50
- data/spec/parser_spec.rb +0 -8
- data/spec/profile_spec.rb +0 -5
@@ -0,0 +1,108 @@
|
|
1
|
+
module Piggly
|
2
|
+
module Reporter
|
3
|
+
|
4
|
+
class Index < Base
|
5
|
+
|
6
|
+
def initialize(config, profile)
|
7
|
+
@config, @profile = config, profile
|
8
|
+
end
|
9
|
+
|
10
|
+
def report(procedures, index)
|
11
|
+
io = File.open("#{report_path}/index.html", "w")
|
12
|
+
|
13
|
+
html(io) do
|
14
|
+
tag :html do
|
15
|
+
tag :head do
|
16
|
+
tag :title, "Piggly PL/pgSQL Code Coverage"
|
17
|
+
tag :link, :rel => "stylesheet", :type => "text/css", :href => "piggly.css"
|
18
|
+
tag :script, "<!-- -->", :type => "text/javascript", :src => "sortable.js"
|
19
|
+
end
|
20
|
+
|
21
|
+
tag :body do
|
22
|
+
aggregate("PL/pgSQL Coverage Summary", @profile.summary)
|
23
|
+
table(procedures.sort_by{|p| index.label(p) }, index)
|
24
|
+
timestamp
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
ensure
|
29
|
+
io.close
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def table(procedures, index)
|
35
|
+
tag :table, :class => "summary sortable" do
|
36
|
+
tag :tr do
|
37
|
+
tag :th, "Procedure"
|
38
|
+
tag :th, "Blocks"
|
39
|
+
tag :th, "Loops"
|
40
|
+
tag :th, "Branches"
|
41
|
+
tag :th, "Block Coverage"
|
42
|
+
tag :th, "Loop Coverage"
|
43
|
+
tag :th, "Branch Coverage"
|
44
|
+
end
|
45
|
+
|
46
|
+
procedures.each_with_index do |procedure, k|
|
47
|
+
summary = @profile.summary(procedure)
|
48
|
+
row = k.modulo(2) == 0 ? "even" : "odd"
|
49
|
+
label = index.label(procedure)
|
50
|
+
|
51
|
+
tag :tr, :class => row do
|
52
|
+
unless summary.include?(:block) or summary.include?(:loop) or summary.include?(:branch)
|
53
|
+
# Parser couldn't parse this file
|
54
|
+
tag :td, label, :class => "file fail"
|
55
|
+
tag(:td, :class => "count") { tag :span, -1, :style => "display:none" }
|
56
|
+
tag(:td, :class => "count") { tag :span, -1, :style => "display:none" }
|
57
|
+
tag(:td, :class => "count") { tag :span, -1, :style => "display:none" }
|
58
|
+
tag(:td, :class => "pct") { tag :span, -1, :style => "display:none" }
|
59
|
+
tag(:td, :class => "pct") { tag :span, -1, :style => "display:none" }
|
60
|
+
tag(:td, :class => "pct") { tag :span, -1, :style => "display:none" }
|
61
|
+
else
|
62
|
+
tag(:td, :class => "file") { tag :a, label, :href => procedure.identifier + ".html" }
|
63
|
+
tag :td, (summary[:block][:count] || 0), :class => "count"
|
64
|
+
tag :td, (summary[:loop][:count] || 0), :class => "count"
|
65
|
+
tag :td, (summary[:branch][:count] || 0), :class => "count"
|
66
|
+
tag(:td, :class => "pct") { percent(summary[:block][:percent]) }
|
67
|
+
tag(:td, :class => "pct") { percent(summary[:loop][:percent]) }
|
68
|
+
tag(:td, :class => "pct") { percent(summary[:branch][:percent]) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def percent(pct)
|
77
|
+
if pct
|
78
|
+
tag :table, :align => "center" do
|
79
|
+
tag :tr do
|
80
|
+
tag :td, "%0.2f%% " % pct, :class => "num"
|
81
|
+
|
82
|
+
style =
|
83
|
+
case pct.to_f
|
84
|
+
when 0...50; "low"
|
85
|
+
when 0...100; "mid"
|
86
|
+
else "high"
|
87
|
+
end
|
88
|
+
|
89
|
+
tag :td, :class => "graph" do
|
90
|
+
if pct
|
91
|
+
tag :table, :align => "right", :class => "graph #{style}" do
|
92
|
+
tag :tr do
|
93
|
+
tag :td, :class => "covered", :width => (pct/2.0).to_i
|
94
|
+
tag :td, :class => "uncovered", :width => ((100-pct)/2.0).to_i
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
tag :span, -1, :style => "display:none"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Piggly
|
2
|
+
module Reporter
|
3
|
+
|
4
|
+
class Procedure < Base
|
5
|
+
|
6
|
+
def initialize(config, profile)
|
7
|
+
@config, @profile = config, profile
|
8
|
+
end
|
9
|
+
|
10
|
+
def report(procedure)
|
11
|
+
io = File.open(report_path(procedure.source_path(@config), ".html"), "w")
|
12
|
+
|
13
|
+
begin
|
14
|
+
compiler = Compiler::CoverageReport.new(@config)
|
15
|
+
data = compiler.compile(procedure, @profile)
|
16
|
+
|
17
|
+
html(io) do
|
18
|
+
tag :html, :xmlns => "http://www.w3.org/1999/xhtml" do
|
19
|
+
tag :head do
|
20
|
+
tag :title, "Code Coverage: #{procedure.name}"
|
21
|
+
tag :link, :rel => "stylesheet", :type => "text/css", :href => "piggly.css"
|
22
|
+
tag :script, "<!-- -->", :type => "text/javascript", :src => "highlight.js"
|
23
|
+
end
|
24
|
+
|
25
|
+
tag :body do
|
26
|
+
aggregate(procedure.name, @profile.summary(procedure))
|
27
|
+
|
28
|
+
tag :div, :class => "listing" do
|
29
|
+
tag :table do
|
30
|
+
tag :tr do
|
31
|
+
tag :td, " ", :class => "signature"
|
32
|
+
tag :td, signature(procedure), :class => "signature"
|
33
|
+
end
|
34
|
+
|
35
|
+
tag :tr do
|
36
|
+
tag :td, data[:lines].to_a.map{|n| %[<a href="#L#{n}" id="L#{n}">#{n}</a>] }.join("\n"), :class => "lines"
|
37
|
+
tag :td, data[:html], :class => "code"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
toc(@profile[procedure])
|
43
|
+
|
44
|
+
timestamp
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
ensure
|
49
|
+
io.close
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def signature(procedure)
|
56
|
+
string = "<span class='tK'>CREATE FUNCTION</span> <b><span class='tI'>#{procedure.name}</span></b>"
|
57
|
+
|
58
|
+
if procedure.arg_names.size <= 1
|
59
|
+
string << " ( "
|
60
|
+
separator = ", "
|
61
|
+
spacer = " "
|
62
|
+
else
|
63
|
+
string << "\n\t( "
|
64
|
+
separator = ",\n\t "
|
65
|
+
spacer = "\t"
|
66
|
+
end
|
67
|
+
|
68
|
+
arguments = procedure.arg_types.zip(procedure.arg_modes, procedure.arg_names).map do |atype, amode, aname|
|
69
|
+
amode &&= "<span class='tK'>#{amode.upcase}</span>#{spacer}"
|
70
|
+
aname &&= "<span class='tI'>#{aname}</span>#{spacer}"
|
71
|
+
"#{amode}#{aname}<span class='tD'>#{atype}</span>"
|
72
|
+
end.join(separator)
|
73
|
+
|
74
|
+
string << arguments << " )"
|
75
|
+
string << "\n<span class='tK'>RETURNS#{procedure.setof ? ' SETOF' : ''}</span>"
|
76
|
+
string << " <span class='tD'>#{procedure.type.shorten}</span>"
|
77
|
+
string << "\n <span class='tK'>SECURITY DEFINER</span>" if procedure.secdef
|
78
|
+
string << "\n <span class='tK'>STRICT</span>" if procedure.strict
|
79
|
+
string << "\n <span class='tK'>#{procedure.volatility.upcase}</span>"
|
80
|
+
|
81
|
+
string
|
82
|
+
end
|
83
|
+
|
84
|
+
def toc(tags)
|
85
|
+
todo = tags.reject{|t| t.complete? }
|
86
|
+
|
87
|
+
tag :div, :class => 'toc' do
|
88
|
+
tag :a, 'Index', :href => 'index.html'
|
89
|
+
|
90
|
+
tag :ol do
|
91
|
+
todo.each do |t|
|
92
|
+
tag(:li, :class => t.type) do
|
93
|
+
tag :a, t.description, :href => "#T#{t.id}",
|
94
|
+
:onMouseOver => "highlight('T#{t.id}')"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end unless todo.empty?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
var lastId = null;
|
3
|
+
var lastBg = null;
|
4
|
+
|
5
|
+
function highlight(id) {
|
6
|
+
var el = document.getElementById(id);
|
7
|
+
if (el) {
|
8
|
+
if (lastId)
|
9
|
+
unhighlight(lastId, lastBg);
|
10
|
+
|
11
|
+
lastId = id;
|
12
|
+
lastBg = el.style.backgroundColor;
|
13
|
+
|
14
|
+
el.style.backgroundColor = 'yellow';
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
function unhighlight(id, bg) {
|
19
|
+
var el = document.getElementById(id);
|
20
|
+
if (el) { el.style.backgroundColor = bg; }
|
21
|
+
}
|
@@ -1,22 +1,30 @@
|
|
1
|
+
/* default piggly style sheet */
|
2
|
+
|
1
3
|
* { margin: 0; padding: 0; }
|
2
4
|
|
3
5
|
body
|
4
6
|
{
|
5
7
|
color: #000;
|
6
8
|
background-color: #fff;
|
7
|
-
padding:
|
9
|
+
padding: 50px;
|
8
10
|
}
|
9
11
|
|
10
12
|
a img { border: 0; }
|
11
13
|
|
12
|
-
div.timestamp
|
14
|
+
div.timestamp
|
15
|
+
{
|
16
|
+
clear: both;
|
17
|
+
font-family: "Consolas", "DejaVu Sans Mono", "Monaco", "Nimbus Mono L", "Courier New";
|
18
|
+
font-style: italic;
|
19
|
+
font-size: 9pt;
|
20
|
+
}
|
13
21
|
|
14
22
|
div.toc
|
15
23
|
{
|
16
24
|
width: 200px;
|
17
25
|
height: 150px;
|
18
26
|
border: 1px solid #c00;
|
19
|
-
background-color: #
|
27
|
+
background-color: #fdd;
|
20
28
|
padding: 5px;
|
21
29
|
position: fixed;
|
22
30
|
overflow: auto;
|
@@ -41,6 +49,21 @@ div.listing
|
|
41
49
|
}
|
42
50
|
|
43
51
|
div.listing table { border-collapse: collapse; }
|
52
|
+
div.listing td.signature
|
53
|
+
{
|
54
|
+
vertical-align: top;
|
55
|
+
padding: 2px 4px;
|
56
|
+
white-space: pre;
|
57
|
+
|
58
|
+
margin: 0;
|
59
|
+
overflow: visible;
|
60
|
+
|
61
|
+
border-bottom: 1px solid silver;
|
62
|
+
background-color: #ffffe0;
|
63
|
+
color: #000;
|
64
|
+
font-family: "Consolas", "DejaVu Sans Mono", "Monaco", "Nimbus Mono L", "Courier New";
|
65
|
+
font-size: 9pt;
|
66
|
+
}
|
44
67
|
div.listing td.code
|
45
68
|
{
|
46
69
|
vertical-align: top;
|
@@ -54,7 +77,7 @@ div.listing td.code
|
|
54
77
|
overflow: visible;
|
55
78
|
|
56
79
|
color: #000;
|
57
|
-
font-family: "DejaVu Sans Mono", "Monaco", "
|
80
|
+
font-family: "Consolas", "DejaVu Sans Mono", "Monaco", "Nimbus Mono L", "Courier New";
|
58
81
|
font-size: 9pt;
|
59
82
|
}
|
60
83
|
|
@@ -70,23 +93,40 @@ div.listing td.lines
|
|
70
93
|
|
71
94
|
background-color: #def;
|
72
95
|
font-size: 9pt;
|
73
|
-
font-family: "DejaVu Sans Mono", "Monaco", "
|
96
|
+
font-family: "Consolas", "DejaVu Sans Mono", "Monaco", "Nimbus Mono L", "Courier New";
|
74
97
|
}
|
75
98
|
div.listing td.lines a { color: grey; text-decoration: none; }
|
76
99
|
|
77
|
-
|
100
|
+
p.summary
|
101
|
+
{
|
102
|
+
font-weight: bold;
|
103
|
+
font-family: "Tahoma", "Trebuchet MS", "Arial", sans-serif;
|
104
|
+
margin-bottom: 1em;
|
105
|
+
}
|
106
|
+
|
107
|
+
table.summary { margin-bottom: 2em; }
|
108
|
+
table.summary th { padding: 5px; border-bottom: 1px solid silver; font-weight: bold; font-size: 12px; background-color: #eee; }
|
78
109
|
table.summary td { padding: 5px; font-size: 10pt; }
|
79
110
|
|
80
|
-
table.summary
|
111
|
+
table.summary tr.odd { border-bottom: 1px solid #eee; }
|
112
|
+
table.summary tr.even { border-bottom: 1px solid #eee; }
|
113
|
+
table.summary td.file { text-align: left; }
|
81
114
|
table.summary td.fail { font-weight: bold; color: #f00; }
|
82
|
-
table.summary td.count { max-width: 50px; min-width: 50px; text-align:
|
83
|
-
table.summary td.pct { max-width: 100px; min-width: 100px;
|
115
|
+
table.summary td.count { max-width: 50px; min-width: 50px; text-align: center; }
|
116
|
+
table.summary td.pct { max-width: 100px; min-width: 100px; }
|
84
117
|
|
85
|
-
table.summary td.pct td.num { padding: 0; max-width: 50px; min-width: 50px; text-align: right; border: 0; }
|
118
|
+
table.summary td.pct td.num { padding: 0; max-width: 50px; min-width: 50px; text-align: right; border: 0; font-size: 10px; }
|
86
119
|
table.summary td.pct td.graph { padding: 0; max-width: 50px; min-width: 50px; text-align: right; border: 0; }
|
87
120
|
|
88
|
-
table.graph td.uncovered {
|
89
|
-
table.
|
121
|
+
table.graph td.uncovered { border: 0; padding: 0; }
|
122
|
+
table.low td.uncovered { background-color: #c00; }
|
123
|
+
table.mid td.uncovered { background-color: #fc0; }
|
124
|
+
table.high td.uncovered { background-color: #093; }
|
125
|
+
|
126
|
+
table.graph td.covered { border: 0; padding: 0; }
|
127
|
+
table.low td.covered { background-color: #f66; }
|
128
|
+
table.mid td.covered { background-color: #ffc; }
|
129
|
+
table.high td.covered { background-color: #390; }
|
90
130
|
|
91
131
|
table.summary
|
92
132
|
{
|
File without changes
|
data/lib/piggly/tags.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
module Piggly
|
2
|
+
#
|
3
|
+
# Coverage is tracked by attaching these compiler-generated tags to various nodes in a stored
|
4
|
+
# procedure's parse tree. These tags each have a unique string identifier which is printed by
|
5
|
+
# various parts of the recompiled stored procedure, and the output is then recognized by
|
6
|
+
# Profile.notice_processor, which calls #ping on the tag corresponding to the printed string.
|
7
|
+
#
|
8
|
+
# After test execution is complete, each AST is walked and Tag values attached to NodeClass
|
9
|
+
# values are used to produce the coverage report
|
10
|
+
#
|
11
|
+
module Tags
|
12
|
+
|
13
|
+
class AbstractTag
|
14
|
+
PATTERN = /[0-9a-f]{16}/
|
15
|
+
|
16
|
+
attr_accessor :id
|
17
|
+
|
18
|
+
def initialize(prefix = nil, id = nil)
|
19
|
+
@id = Digest::MD5.hexdigest(prefix.to_s + (id || object_id).to_s).slice(0, 16)
|
20
|
+
end
|
21
|
+
|
22
|
+
alias to_s id
|
23
|
+
|
24
|
+
# Defined here in case ActiveSupport hasn't defined it on Object
|
25
|
+
def tap
|
26
|
+
yield self
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class EvaluationTag < AbstractTag
|
32
|
+
attr_reader :ran
|
33
|
+
|
34
|
+
def initialize(*args)
|
35
|
+
clear
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
:block
|
41
|
+
end
|
42
|
+
|
43
|
+
def ping(value)
|
44
|
+
@ran = true
|
45
|
+
end
|
46
|
+
|
47
|
+
def style
|
48
|
+
"c#{@ran ? "1" : "0"}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_f
|
52
|
+
@ran ? 100.0 : 0.0
|
53
|
+
end
|
54
|
+
|
55
|
+
def complete?
|
56
|
+
@ran
|
57
|
+
end
|
58
|
+
|
59
|
+
def description
|
60
|
+
@ran ? "full coverage" : "never evaluated"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Resets code coverage
|
64
|
+
def clear
|
65
|
+
@ran = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def ==(other)
|
69
|
+
@id == other.id and @ran == other.ran
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Tracks a contiguous sequence of statements
|
75
|
+
#
|
76
|
+
class BlockTag < EvaluationTag
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Tracks procedure calls, raise exception, exits, returns
|
81
|
+
#
|
82
|
+
class UnconditionalBranchTag < EvaluationTag
|
83
|
+
# Aggregate this coverage data with conditional branches
|
84
|
+
def type
|
85
|
+
:branch
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Tracks if, catch, case branch, continue when, and exit when statements
|
91
|
+
# where the coverage consists of the condition evaluating true and false
|
92
|
+
#
|
93
|
+
class ConditionalBranchTag < AbstractTag
|
94
|
+
attr_reader :true, :false
|
95
|
+
|
96
|
+
def initialize(*args)
|
97
|
+
clear
|
98
|
+
super
|
99
|
+
end
|
100
|
+
|
101
|
+
def type
|
102
|
+
:branch
|
103
|
+
end
|
104
|
+
|
105
|
+
def ping(value)
|
106
|
+
case value
|
107
|
+
when "t"; @true = true
|
108
|
+
when "f"; @false = true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def style
|
113
|
+
"b#{@true ? 1 : 0}#{@false ? 1 : 0 }"
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_f
|
117
|
+
(@true and @false) ? 100.0 : (@true or @false) ? 50.0 : 0.0
|
118
|
+
end
|
119
|
+
|
120
|
+
def complete?
|
121
|
+
@true and @false
|
122
|
+
end
|
123
|
+
|
124
|
+
def description
|
125
|
+
if @true and @false
|
126
|
+
"full coverage"
|
127
|
+
elsif @true
|
128
|
+
"never evaluates false"
|
129
|
+
elsif @false
|
130
|
+
"never evaluates true"
|
131
|
+
else
|
132
|
+
"never evaluated"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def clear
|
137
|
+
@true, @false = false
|
138
|
+
end
|
139
|
+
|
140
|
+
def ==(other)
|
141
|
+
@id == other.id and @true == other.true and @false == other.false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# Tracks loops where coverage consists of iterating once, iterating more
|
147
|
+
# than once, passing through, and at least one full iteration
|
148
|
+
#
|
149
|
+
class AbstractLoopTag < AbstractTag
|
150
|
+
def self.states
|
151
|
+
{ # Never terminates normally (so @pass must be false)
|
152
|
+
0b0000 => "never evaluated",
|
153
|
+
0b0001 => "iterations always terminate early. loop always iterates more than once",
|
154
|
+
0b0010 => "iterations always terminate early. loop always iterates only once",
|
155
|
+
0b0011 => "iterations always terminate early",
|
156
|
+
# Terminates normally (one of @pass, @once, @twice must be true)
|
157
|
+
0b1001 => "loop always iterates more than once",
|
158
|
+
0b1010 => "loop always iterates only once",
|
159
|
+
0b1011 => "loop never passes through",
|
160
|
+
0b1100 => "loop always passes through",
|
161
|
+
0b1101 => "loop never iterates only once",
|
162
|
+
0b1110 => "loop never iterates more than once",
|
163
|
+
0b1111 => "full coverage" }
|
164
|
+
end
|
165
|
+
|
166
|
+
attr_reader :pass, :once, :twice, :ends, :count
|
167
|
+
|
168
|
+
def initialize(*args)
|
169
|
+
clear
|
170
|
+
super
|
171
|
+
end
|
172
|
+
|
173
|
+
def type
|
174
|
+
:loop
|
175
|
+
end
|
176
|
+
|
177
|
+
def style
|
178
|
+
"l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def to_f
|
182
|
+
# Value space:
|
183
|
+
# (1,2,X) - loop iterated at least twice and terminated normally
|
184
|
+
# (1,X) - loop iterated only once and terminated normally
|
185
|
+
# (0,X) - loop never iterated and terminated normally (pass-thru)
|
186
|
+
# () - loop condition was never executed
|
187
|
+
#
|
188
|
+
# These combinations are ignored, because coverage will probably not reveal bugs
|
189
|
+
# (1,2) - loop iterated at least twice but terminated early
|
190
|
+
# (1) - loop iterated only once but terminated early
|
191
|
+
100 * (Util::Enumerable.count([@pass, @once, @twice, @ends]){|x| x } / 4.0)
|
192
|
+
end
|
193
|
+
|
194
|
+
def complete?
|
195
|
+
@pass and @once and @twice and @ends
|
196
|
+
end
|
197
|
+
|
198
|
+
def description
|
199
|
+
self.class.states.fetch(n = state, "unknown tag state: #{n}")
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns state represented as a 4-bit integer
|
203
|
+
def state
|
204
|
+
[@ends,@pass,@once,@twice].reverse.inject([0,0]){|(k,n), bit| [k + 1, n | (bit ? 1 : 0) << k] }.last
|
205
|
+
end
|
206
|
+
|
207
|
+
def clear
|
208
|
+
@pass = false
|
209
|
+
@once = false
|
210
|
+
@twice = false
|
211
|
+
@ends = false
|
212
|
+
@count = 0
|
213
|
+
end
|
214
|
+
|
215
|
+
def ==(other)
|
216
|
+
@id == other.id and
|
217
|
+
@ends == other.ends and
|
218
|
+
@pass == other.pass and
|
219
|
+
@once == other.once and
|
220
|
+
@twice == other.twice
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# Tracks loops that have a boolean condition in the loop statement (WHILE loops)
|
226
|
+
#
|
227
|
+
class ConditionalLoopTag < AbstractLoopTag
|
228
|
+
def ping(value)
|
229
|
+
case value
|
230
|
+
when "t"
|
231
|
+
# Loop iterated
|
232
|
+
@count += 1
|
233
|
+
else
|
234
|
+
# Loop terminated
|
235
|
+
case @count
|
236
|
+
when 0; @pass = true
|
237
|
+
when 1; @once = true
|
238
|
+
else; @twice = true
|
239
|
+
end
|
240
|
+
@count = 0
|
241
|
+
|
242
|
+
# This isn't accurate. there needs to be a signal at the end
|
243
|
+
# of the loop body to indicate it was reached. Otherwise its
|
244
|
+
# possible each iteration restarts early with CONTINUE
|
245
|
+
@ends = true
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# Tracks loops that don't have a boolean condition in the loop statement (LOOP and FOR loops)
|
252
|
+
#
|
253
|
+
class UnconditionalLoopTag < AbstractLoopTag
|
254
|
+
def self.states
|
255
|
+
super.merge \
|
256
|
+
0b0100 => "loop always passes through"
|
257
|
+
end
|
258
|
+
|
259
|
+
def ping(value)
|
260
|
+
case value
|
261
|
+
when "t"
|
262
|
+
# start of iteration
|
263
|
+
@count += 1
|
264
|
+
when "@"
|
265
|
+
# end of iteration
|
266
|
+
@ends = true
|
267
|
+
when "f"
|
268
|
+
# loop exit
|
269
|
+
case @count
|
270
|
+
when 0; @pass = true
|
271
|
+
when 1; @once = true
|
272
|
+
else; @twice = true
|
273
|
+
end
|
274
|
+
@count = 0
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|