piggly 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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