piggly 1.2.1 → 2.0.0

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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +163 -0
  3. data/Rakefile +29 -15
  4. data/bin/piggly +4 -244
  5. data/lib/piggly.rb +19 -17
  6. data/lib/piggly/command.rb +9 -0
  7. data/lib/piggly/command/base.rb +148 -0
  8. data/lib/piggly/command/report.rb +162 -0
  9. data/lib/piggly/command/test.rb +157 -0
  10. data/lib/piggly/command/trace.rb +90 -0
  11. data/lib/piggly/command/untrace.rb +78 -0
  12. data/lib/piggly/compiler.rb +7 -5
  13. data/lib/piggly/compiler/cache_dir.rb +119 -0
  14. data/lib/piggly/compiler/coverage_report.rb +63 -0
  15. data/lib/piggly/compiler/trace_compiler.rb +105 -0
  16. data/lib/piggly/config.rb +47 -22
  17. data/lib/piggly/dumper.rb +9 -0
  18. data/lib/piggly/dumper/index.rb +121 -0
  19. data/lib/piggly/dumper/qualified_name.rb +36 -0
  20. data/lib/piggly/dumper/qualified_type.rb +81 -0
  21. data/lib/piggly/dumper/reified_procedure.rb +142 -0
  22. data/lib/piggly/dumper/skeleton_procedure.rb +102 -0
  23. data/lib/piggly/installer.rb +84 -42
  24. data/lib/piggly/parser.rb +43 -49
  25. data/lib/piggly/parser/grammar.tt +289 -313
  26. data/lib/piggly/parser/nodes.rb +270 -211
  27. data/lib/piggly/parser/traversal.rb +35 -33
  28. data/lib/piggly/parser/treetop_ruby19_patch.rb +1 -1
  29. data/lib/piggly/profile.rb +81 -60
  30. data/lib/piggly/reporter.rb +5 -18
  31. data/lib/piggly/reporter/base.rb +103 -0
  32. data/lib/piggly/reporter/html_dsl.rb +63 -0
  33. data/lib/piggly/reporter/index.rb +108 -0
  34. data/lib/piggly/reporter/procedure.rb +104 -0
  35. data/lib/piggly/reporter/resources/highlight.js +21 -0
  36. data/lib/piggly/reporter/{piggly.css → resources/piggly.css} +52 -12
  37. data/lib/piggly/reporter/{sortable.js → resources/sortable.js} +0 -0
  38. data/lib/piggly/tags.rb +280 -0
  39. data/lib/piggly/task.rb +191 -40
  40. data/lib/piggly/util.rb +8 -27
  41. data/lib/piggly/util/blankslate.rb +114 -0
  42. data/lib/piggly/util/cacheable.rb +19 -0
  43. data/lib/piggly/util/enumerable.rb +44 -0
  44. data/lib/piggly/util/file.rb +17 -0
  45. data/lib/piggly/util/process_queue.rb +96 -0
  46. data/lib/piggly/util/thunk.rb +39 -0
  47. data/lib/piggly/version.rb +8 -8
  48. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  49. data/spec/examples/compiler/report_spec.rb +25 -0
  50. data/spec/{compiler → examples/compiler}/trace_spec.rb +7 -57
  51. data/spec/examples/config_spec.rb +61 -0
  52. data/spec/examples/dumper/index_spec.rb +197 -0
  53. data/spec/examples/dumper/procedure_spec.rb +116 -0
  54. data/spec/{grammar → examples/grammar}/expression_spec.rb +60 -60
  55. data/spec/{grammar → examples/grammar}/statements/assignment_spec.rb +15 -15
  56. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  57. data/spec/{grammar → examples/grammar}/statements/exception_spec.rb +10 -10
  58. data/spec/{grammar → examples/grammar}/statements/if_spec.rb +47 -34
  59. data/spec/{grammar → examples/grammar}/statements/loop_spec.rb +5 -5
  60. data/spec/{grammar → examples/grammar}/statements/sql_spec.rb +11 -11
  61. data/spec/{grammar → examples/grammar}/tokens/comment_spec.rb +11 -11
  62. data/spec/{grammar → examples/grammar}/tokens/datatype_spec.rb +14 -8
  63. data/spec/{grammar → examples/grammar}/tokens/identifier_spec.rb +26 -10
  64. data/spec/{grammar → examples/grammar}/tokens/keyword_spec.rb +5 -5
  65. data/spec/{grammar → examples/grammar}/tokens/label_spec.rb +7 -7
  66. data/spec/{grammar → examples/grammar}/tokens/literal_spec.rb +1 -1
  67. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  68. data/spec/{grammar → examples/grammar}/tokens/number_spec.rb +1 -1
  69. data/spec/{grammar → examples/grammar}/tokens/sqlkeywords_spec.rb +1 -1
  70. data/spec/{grammar → examples/grammar}/tokens/string_spec.rb +9 -9
  71. data/spec/{grammar → examples/grammar}/tokens/whitespace_spec.rb +1 -1
  72. data/spec/examples/installer_spec.rb +59 -0
  73. data/spec/examples/parser/nodes_spec.rb +73 -0
  74. data/spec/examples/parser/traversal_spec.rb +14 -0
  75. data/spec/examples/parser_spec.rb +115 -0
  76. data/spec/examples/profile_spec.rb +153 -0
  77. data/spec/{reporter/html_spec.rb → examples/reporter/html/dsl_spec.rb} +0 -0
  78. data/spec/examples/reporter/html/index_spec.rb +0 -0
  79. data/spec/examples/reporter/html_spec.rb +1 -0
  80. data/spec/examples/reporter_spec.rb +0 -0
  81. data/spec/{compiler → examples}/tags_spec.rb +10 -10
  82. data/spec/examples/task_spec.rb +0 -0
  83. data/spec/examples/util/cacheable_spec.rb +41 -0
  84. data/spec/examples/util/enumerable_spec.rb +64 -0
  85. data/spec/examples/util/file_spec.rb +40 -0
  86. data/spec/examples/util/process_queue_spec.rb +16 -0
  87. data/spec/examples/util/thunk_spec.rb +58 -0
  88. data/spec/examples/version_spec.rb +0 -0
  89. data/spec/issues/007_spec.rb +25 -0
  90. data/spec/issues/008_spec.rb +73 -0
  91. data/spec/issues/018_spec.rb +25 -0
  92. data/spec/spec_helper.rb +253 -9
  93. metadata +136 -93
  94. data/README.markdown +0 -116
  95. data/lib/piggly/compiler/cache.rb +0 -151
  96. data/lib/piggly/compiler/pretty.rb +0 -67
  97. data/lib/piggly/compiler/queue.rb +0 -46
  98. data/lib/piggly/compiler/tags.rb +0 -244
  99. data/lib/piggly/compiler/trace.rb +0 -91
  100. data/lib/piggly/filecache.rb +0 -40
  101. data/lib/piggly/parser/parser.rb +0 -11794
  102. data/lib/piggly/reporter/html.rb +0 -207
  103. data/spec/compiler/cache_spec.rb +0 -9
  104. data/spec/compiler/pretty_spec.rb +0 -9
  105. data/spec/compiler/queue_spec.rb +0 -3
  106. data/spec/compiler/rewrite_spec.rb +0 -3
  107. data/spec/config_spec.rb +0 -58
  108. data/spec/filecache_spec.rb +0 -70
  109. data/spec/fixtures/snippets.sql +0 -158
  110. data/spec/grammar/tokens/lval_spec.rb +0 -50
  111. data/spec/parser_spec.rb +0 -8
  112. 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%%&nbsp;" % 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, "&nbsp;", :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: 8px;
9
+ padding: 50px;
8
10
  }
9
11
 
10
12
  a img { border: 0; }
11
13
 
12
- div.timestamp { clear: both; }
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: #fee;
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", "Consolas", "Nimbus Mono L", "Courier New";
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", "Consolas", "Nimbus Mono L", "Courier New";
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
- table.summary th { padding: 5px; border: 1px solid silver; font-weight: bold; font-size: 12px; background-color: #def; }
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 td.file { text-align: left; border: 0; }
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: right; border: 1px solid silver; }
83
- table.summary td.pct { max-width: 100px; min-width: 100px; text-align: right; border: 0; }
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 { background-color: #669; border: 0px; padding: 0px; }
89
- table.graph td.covered { background-color: #ccf; border: 0px; padding: 0px; }
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
  {
@@ -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