piggly 1.2.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 (59) hide show
  1. data/README.markdown +84 -0
  2. data/Rakefile +19 -0
  3. data/bin/piggly +245 -0
  4. data/lib/piggly/compiler/cache.rb +151 -0
  5. data/lib/piggly/compiler/pretty.rb +67 -0
  6. data/lib/piggly/compiler/queue.rb +46 -0
  7. data/lib/piggly/compiler/tags.rb +244 -0
  8. data/lib/piggly/compiler/trace.rb +91 -0
  9. data/lib/piggly/compiler.rb +5 -0
  10. data/lib/piggly/config.rb +43 -0
  11. data/lib/piggly/filecache.rb +40 -0
  12. data/lib/piggly/installer.rb +95 -0
  13. data/lib/piggly/parser/grammar.tt +747 -0
  14. data/lib/piggly/parser/nodes.rb +319 -0
  15. data/lib/piggly/parser/parser.rb +11783 -0
  16. data/lib/piggly/parser/traversal.rb +48 -0
  17. data/lib/piggly/parser/treetop_ruby19_patch.rb +17 -0
  18. data/lib/piggly/parser.rb +67 -0
  19. data/lib/piggly/profile.rb +87 -0
  20. data/lib/piggly/reporter/html.rb +207 -0
  21. data/lib/piggly/reporter/piggly.css +187 -0
  22. data/lib/piggly/reporter/sortable.js +493 -0
  23. data/lib/piggly/reporter.rb +21 -0
  24. data/lib/piggly/task.rb +64 -0
  25. data/lib/piggly/util.rb +28 -0
  26. data/lib/piggly/version.rb +15 -0
  27. data/lib/piggly.rb +18 -0
  28. data/spec/compiler/cache_spec.rb +9 -0
  29. data/spec/compiler/pretty_spec.rb +9 -0
  30. data/spec/compiler/queue_spec.rb +3 -0
  31. data/spec/compiler/rewrite_spec.rb +3 -0
  32. data/spec/compiler/tags_spec.rb +285 -0
  33. data/spec/compiler/trace_spec.rb +173 -0
  34. data/spec/config_spec.rb +58 -0
  35. data/spec/filecache_spec.rb +70 -0
  36. data/spec/fixtures/snippets.sql +158 -0
  37. data/spec/grammar/expression_spec.rb +302 -0
  38. data/spec/grammar/statements/assignment_spec.rb +70 -0
  39. data/spec/grammar/statements/exception_spec.rb +52 -0
  40. data/spec/grammar/statements/if_spec.rb +178 -0
  41. data/spec/grammar/statements/loop_spec.rb +41 -0
  42. data/spec/grammar/statements/sql_spec.rb +71 -0
  43. data/spec/grammar/tokens/comment_spec.rb +58 -0
  44. data/spec/grammar/tokens/datatype_spec.rb +52 -0
  45. data/spec/grammar/tokens/identifier_spec.rb +58 -0
  46. data/spec/grammar/tokens/keyword_spec.rb +44 -0
  47. data/spec/grammar/tokens/label_spec.rb +40 -0
  48. data/spec/grammar/tokens/literal_spec.rb +30 -0
  49. data/spec/grammar/tokens/lval_spec.rb +50 -0
  50. data/spec/grammar/tokens/number_spec.rb +34 -0
  51. data/spec/grammar/tokens/sqlkeywords_spec.rb +45 -0
  52. data/spec/grammar/tokens/string_spec.rb +54 -0
  53. data/spec/grammar/tokens/whitespace_spec.rb +40 -0
  54. data/spec/parser_spec.rb +8 -0
  55. data/spec/profile_spec.rb +5 -0
  56. data/spec/reporter/html_spec.rb +0 -0
  57. data/spec/spec_helper.rb +61 -0
  58. data/spec/spec_suite.rb +5 -0
  59. metadata +121 -0
@@ -0,0 +1,48 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Routines for traversing a tree; assumes base class defines elements
5
+ # as a method that returns a list of child nodes
6
+ #
7
+ module NodeTraversal
8
+ def fold_down(init) # :yields: NodeClass => init
9
+ if elements
10
+ elements.inject(yield(init, self)) do |state, e|
11
+ e.fold_down(state){|succ, n| yield(succ, n) }
12
+ end
13
+ else
14
+ yield(init, self)
15
+ end
16
+ end
17
+
18
+ def count # :yields: NodeClass => boolean
19
+ fold_down(0){|sum, e| yield(e) ? sum + 1 : sum }
20
+ end
21
+
22
+ def find # :yields: NodeClass => boolean
23
+ found = false
24
+ catch :done do
25
+ fold_down(nil) do |_,e|
26
+ if yield(e)
27
+ found = e
28
+ throw :done
29
+ end
30
+ end
31
+ end
32
+ found
33
+ end
34
+
35
+ def select # :yields: NodeClass => boolean
36
+ fold_down([]){|list,e| yield(e) ? list << e : list }
37
+ end
38
+
39
+ def flatten # :yields: NodeClass
40
+ if block_given?
41
+ fold_down([]){|list,e| list << yield(e) }
42
+ else
43
+ fold_down([]){|list,e| list << e }
44
+ end
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,17 @@
1
+ module Treetop
2
+ module Runtime
3
+ class CompiledParser
4
+
5
+ class Regexp < ::Regexp
6
+ def initialize(*args)
7
+ if args.size == 1
8
+ super(args.first, nil, 'n')
9
+ else
10
+ super
11
+ end
12
+ end
13
+ end if RUBY_VERSION > '1.9.0'
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Pl/pgSQL Parser, returns a tree of NodeClass values (see nodes.rb)
5
+ #
6
+ class Parser
7
+ include FileCache
8
+
9
+ class Failure < RuntimeError; end
10
+
11
+ # Returns parse tree
12
+ def self.parse(string)
13
+ p = parser
14
+
15
+ begin
16
+ # downcase input for case-insensitive parsing,
17
+ # then restore original string after parsing
18
+ input = string.downcase
19
+ tree = p.parse(input)
20
+ tree or raise Failure, "#{p.failure_reason}"
21
+ rescue Failure
22
+ $!.backtrace.clear
23
+ raise
24
+ ensure
25
+ input.replace string
26
+ end
27
+ end
28
+
29
+ def self.parser_path; File.join(File.dirname(__FILE__), 'parser', 'parser.rb') end
30
+ def self.grammar_path; File.join(File.dirname(__FILE__), 'parser', 'grammar.tt') end
31
+ def self.nodes_path; File.join(File.dirname(__FILE__), 'parser', 'nodes.rb') end
32
+
33
+ def self.stale?(source)
34
+ File.stale?(cache_path(source), source, grammar_path, parser_path, nodes_path)
35
+ end
36
+
37
+ def self.cache(source)
38
+ cache = cache_path(source)
39
+
40
+ if stale?(source)
41
+ tree = parse(File.read(source))
42
+ File.open(cache, 'w+') do |f|
43
+ Marshal.dump(tree, f)
44
+ tree
45
+ end
46
+ else
47
+ _ = parser # ensure parser libraries, like nodes.rb, are loaded
48
+ Marshal.load(File.read(cache))
49
+ end
50
+ end
51
+
52
+ # Returns treetop parser (recompiled as needed)
53
+ def self.parser
54
+ require 'treetop'
55
+ require 'piggly/parser/treetop_ruby19_patch'
56
+ require nodes_path
57
+
58
+ if File.stale?(parser_path, grammar_path)
59
+ Treetop::Compiler::GrammarCompiler.new.compile(grammar_path, parser_path)
60
+ end
61
+
62
+ require parser_path
63
+ ::PigglyParser.new
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,87 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Collection of all Tags
5
+ #
6
+ class Profile
7
+ PATTERN = /WARNING: #{Config.trace_prefix} (#{Tag::PATTERN})(?: (.))?/
8
+
9
+ class << self
10
+
11
+ # Build a notice processor function that records each tag execution
12
+ def notice_processor
13
+ proc do |message|
14
+ if m = PATTERN.match(message)
15
+ ping(m.captures[0], m.captures[1])
16
+ else
17
+ STDERR.puts message
18
+ end
19
+ end
20
+ end
21
+
22
+ # Register a source file (path) with its list of tags
23
+ def add(path, tags, cache = nil)
24
+ tags.each{|t| by_id[t.id] = t }
25
+ by_file[path] = tags
26
+ by_cache[cache] = tags if cache
27
+ end
28
+
29
+ # Each tag indexed by unique ID
30
+ def by_id
31
+ @by_id ||= Hash.new
32
+ end
33
+
34
+ # Each tag grouped by source file path
35
+ def by_file
36
+ @by_file ||= Hash.new
37
+ end
38
+
39
+ # Each tag grouped by FileCache
40
+ def by_cache
41
+ @by_cache ||= Hash.new
42
+ end
43
+
44
+ # Record the execution of a coverage tag
45
+ def ping(tag_id, value=nil)
46
+ if tag = by_id[tag_id]
47
+ tag.ping(value)
48
+ else
49
+ raise "No tag with id #{tag_id}, perhaps the proc was not compiled with Piggly::Installer.trace_proc, or it has been recompiled with new tag IDs."
50
+ end
51
+ end
52
+
53
+ # Summarizes coverage for each type of tag (branch, block, loop)
54
+ def summary(file = nil)
55
+ summary = Hash.new{|h,k| h[k] = Hash.new }
56
+
57
+ if file
58
+ if by_file.include?(file)
59
+ grouped = by_file[file].group_by{|t| t.type }
60
+ else
61
+ grouped = {}
62
+ end
63
+ else
64
+ grouped = map.group_by{|t| t.type }
65
+ end
66
+
67
+ grouped.each do |type, ts|
68
+ summary[type][:count] = ts.size
69
+ summary[type][:percent] = ts.sum{|t| t.to_f } / ts.size
70
+ end
71
+
72
+ summary
73
+ end
74
+
75
+ # Resets each tag's coverage stats
76
+ def clear
77
+ by_id.values.each{|t| t.clear }
78
+ end
79
+
80
+ # Write each tag's coverage stats to the disk cache
81
+ def store
82
+ by_cache.each{|cache, tags| cache[:tags] = tags }
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,207 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Markup DSL
5
+ #
6
+ module HtmlTag
7
+ unless defined? HTML_REPLACE
8
+ HTML_REPLACE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
9
+ HTML_PATTERN = /[&"<>]/
10
+ end
11
+
12
+ def html(output = '')
13
+ begin
14
+ @htmltag_output, htmltag_output = output, @htmltag_output
15
+ # TODO: doctype
16
+ yield
17
+ ensure
18
+ # restore
19
+ @htmltag_output = htmltag_output
20
+ end
21
+ end
22
+
23
+ def tag(name, content = nil, attributes = {})
24
+ if content.is_a?(Hash) and attributes.empty?
25
+ content, attributes = nil, content
26
+ end
27
+
28
+ attributes = attributes.inject('') do |string, pair|
29
+ k, v = pair
30
+ string << %[ #{k}="#{v}"]
31
+ end
32
+
33
+ if content.nil?
34
+ if block_given?
35
+ @htmltag_output << "<#{name}#{attributes}>"
36
+ yield
37
+ @htmltag_output << "</#{name}>"
38
+ else
39
+ @htmltag_output << "<#{name}#{attributes}/>"
40
+ end
41
+ else
42
+ @htmltag_output << "<#{name}#{attributes}>#{content.to_s}</#{name}>"
43
+ end
44
+ end
45
+
46
+ if ''.respond_to?(:fast_xs)
47
+ def e(string)
48
+ e.fast_xs
49
+ end
50
+ elsif ''.respond_to?(:to_xs)
51
+ def e(string)
52
+ e.to_xs
53
+ end
54
+ else
55
+ def e(string)
56
+ string.gsub(HTML_PATTERN) {|c| HTML_REPLACE[c] }
57
+ end
58
+ end
59
+ end
60
+
61
+ class HtmlReporter < Reporter
62
+ extend HtmlTag
63
+
64
+ def self.output(path, data, summary)
65
+ File.open(report_path(path, '.html'), 'w') do |f|
66
+ html(f) do
67
+
68
+ tag :html, :xmlns => 'http://www.w3.org/1999/xhtml' do
69
+ tag :head do
70
+ tag :title, "Code Coverage: #{File.basename(path)}"
71
+ tag :link, :rel => 'stylesheet', :type => 'text/css', :href => 'piggly.css'
72
+ end
73
+
74
+ tag :body do
75
+ table(path)
76
+
77
+ tag :br
78
+ tag :div, :class => 'listing' do
79
+ tag :table do
80
+ tag :tr do
81
+ tag :td, data.fetch('lines').to_a.map{|n| %[<a href="#L#{n}" id="L#{n}">#{n}</a>] }.join("\n"), :class => 'lines'
82
+ tag :td, data.fetch('html'), :class => 'code'
83
+ end
84
+ end
85
+ end
86
+
87
+ toc(data.fetch('tags'))
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
94
+
95
+ def self.toc(tags)
96
+ todo = tags.reject{|t| t.complete? }
97
+
98
+ tag :div, :class => 'toc' do
99
+ tag :a, 'Index', :href => 'index.html'
100
+
101
+ unless todo.empty?
102
+ tag :ol do
103
+ todo.each do |t|
104
+ tag(:li, :class => t.type) { tag :a, t.description, :href => "#T#{t.id}" }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def self.timestamp
112
+ tag :div, "Generated by piggly #{Piggly::VERSION} at #{Time.now.strftime('%B %d, %Y %H:%M %Z')}", :class => 'timestamp'
113
+ end
114
+
115
+ def self.table(*files)
116
+ tag :table, :class => 'summary sortable' do
117
+ tag :tr do
118
+ tag :th, 'File'
119
+ tag :th, 'Blocks'
120
+ tag :th, 'Loops'
121
+ tag :th, 'Branches'
122
+ tag :th, 'Block Coverage'
123
+ tag :th, 'Loop Coverage'
124
+ tag :th, 'Branch Coverage'
125
+ end
126
+
127
+ files.each_with_index do |name, index|
128
+ summary = Profile.summary(name)
129
+ row = index.modulo(2) == 0 ? 'even' : 'odd'
130
+
131
+ tag :tr, :class => row do
132
+ unless summary.include?(:block) or summary.include?(:loop) or summary.include?(:branch)
133
+ # PigglyParser couldn't parse this file
134
+ tag :td, File.basename(name), :class => 'file fail'
135
+ tag(:td, :class => 'count') { tag :span, -1, :style => 'display:none' }
136
+ tag(:td, :class => 'count') { tag :span, -1, :style => 'display:none' }
137
+ tag(:td, :class => 'count') { tag :span, -1, :style => 'display:none' }
138
+ tag(:td, :class => 'pct') { tag :span, -1, :style => 'display:none' }
139
+ tag(:td, :class => 'pct') { tag :span, -1, :style => 'display:none' }
140
+ tag(:td, :class => 'pct') { tag :span, -1, :style => 'display:none' }
141
+ else
142
+ tag(:td, :class => 'file') { tag :a, File.basename(name), :href => File.basename(name, '.*') + '.html' }
143
+ tag :td, (summary[:block][:count] || 0), :class => 'count'
144
+ tag :td, (summary[:loop][:count] || 0), :class => 'count'
145
+ tag :td, (summary[:branch][:count] || 0), :class => 'count'
146
+ tag(:td, :class => 'pct') { percent(summary[:block][:percent]) }
147
+ tag(:td, :class => 'pct') { percent(summary[:loop][:percent]) }
148
+ tag(:td, :class => 'pct') { percent(summary[:branch][:percent]) }
149
+ end
150
+ end
151
+
152
+ end
153
+ end
154
+ end
155
+
156
+ def self.percent(pct)
157
+ if pct
158
+ tag :table, :align => 'center' do
159
+ tag :tr do
160
+
161
+ tag :td, '%0.2f%%&nbsp;' % pct, :class => 'num'
162
+ tag :td, :class => 'graph' do
163
+ if pct
164
+ tag :table, :align => 'right', :class => 'graph' do
165
+ tag :tr do
166
+ tag :td, :class => 'covered', :width => (pct/2.0).to_i
167
+ tag :td, :class => 'uncovered', :width => ((100-pct)/2.0).to_i
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ end
174
+ end
175
+ else
176
+ tag :span, -1, :style => 'display:none'
177
+ end
178
+ end
179
+
180
+ class Index < HtmlReporter
181
+ extend HtmlTag
182
+
183
+ def self.output(sources)
184
+ File.open(File.join(report_path, 'index.html'), 'w') do |f|
185
+ html(f) do
186
+
187
+ tag :html do
188
+ tag :head do
189
+ tag :title, 'Piggly PL/pgSQL Code Coverage'
190
+ tag :link, :rel => 'stylesheet', :type => 'text/css', :href => 'piggly.css'
191
+ tag :script, '<!-- -->', :type => 'text/javascript', :src => 'sortable.js'
192
+ end
193
+
194
+ tag :body do
195
+ table(*sources.sort)
196
+ tag :br
197
+ timestamp
198
+ end
199
+ end
200
+
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ end
207
+ end
@@ -0,0 +1,187 @@
1
+ * { margin: 0; padding: 0; }
2
+
3
+ body
4
+ {
5
+ color: #000;
6
+ background-color: #fff;
7
+ padding: 8px;
8
+ }
9
+
10
+ a img { border: 0; }
11
+
12
+ div.timestamp { clear: both; }
13
+
14
+ div.toc
15
+ {
16
+ width: 200px;
17
+ height: 150px;
18
+ border: 1px solid #c00;
19
+ background-color: #fee;
20
+ padding: 5px;
21
+ position: fixed;
22
+ overflow: auto;
23
+ bottom: 10px;
24
+ right: 10px;
25
+ font-family: "Tahoma", "Trebuchet MS", "Arial", sans-serif;
26
+ }
27
+ div.toc ol { margin-left: 1.6em; }
28
+ div.toc a { color: #c00; }
29
+ div.toc li { font-size: 75%; }
30
+ div.toc li.branch { }
31
+ div.toc li.block { }
32
+ div.toc li.loop { }
33
+
34
+ /* main container for line numbers and code */
35
+ div.listing
36
+ {
37
+ background-color: #f9f9f9;
38
+ border: 1px solid silver;
39
+ margin: 0 0 1.5em 0;
40
+ overflow: auto;
41
+ }
42
+
43
+ div.listing table { border-collapse: collapse; }
44
+ div.listing td.code
45
+ {
46
+ vertical-align: top;
47
+ padding: 2px 4px;
48
+ white-space: pre;
49
+
50
+ margin: 0;
51
+ width: 100%;
52
+ float: none;
53
+ clear: none;
54
+ overflow: visible;
55
+
56
+ color: #000;
57
+ font-family: "DejaVu Sans Mono", "Monaco", "Consolas", "Nimbus Mono L", "Courier New";
58
+ font-size: 9pt;
59
+ }
60
+
61
+ /* line numbers */
62
+ div.listing td.lines
63
+ {
64
+ vertical-align: top;
65
+ padding: 2px 4px;
66
+ white-space: pre;
67
+
68
+ text-align: right;
69
+ overflow: visible;
70
+
71
+ background-color: #def;
72
+ font-size: 9pt;
73
+ font-family: "DejaVu Sans Mono", "Monaco", "Consolas", "Nimbus Mono L", "Courier New";
74
+ }
75
+ div.listing td.lines a { color: grey; text-decoration: none; }
76
+
77
+ table.summary th { padding: 5px; border: 1px solid silver; font-weight: bold; font-size: 12px; background-color: #def; }
78
+ table.summary td { padding: 5px; font-size: 10pt; }
79
+
80
+ table.summary td.file { text-align: left; border: 0; }
81
+ 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; }
84
+
85
+ table.summary td.pct td.num { padding: 0; max-width: 50px; min-width: 50px; text-align: right; border: 0; }
86
+ table.summary td.pct td.graph { padding: 0; max-width: 50px; min-width: 50px; text-align: right; border: 0; }
87
+
88
+ table.graph td.uncovered { background-color: #669; border: 0px; padding: 0px; }
89
+ table.graph td.covered { background-color: #ccf; border: 0px; padding: 0px; }
90
+
91
+ table.summary
92
+ {
93
+ font-family: "Tahoma", "Trebuchet MS", "Arial", sans-serif;
94
+ width: 100%;
95
+ border-spacing: 0;
96
+ border-collapse: collapse;
97
+ }
98
+
99
+ table.summary td.pct table
100
+ {
101
+ font-size: 85%;
102
+ font-family: "Tahoma", "Trebuchet MS", "Arial", sans-serif;
103
+ padding: 0;
104
+ border-spacing: 0;
105
+ border-collapse: collapse;
106
+ }
107
+
108
+ table.graph
109
+ {
110
+ min-width: 50px;
111
+ max-width: 50px;
112
+ padding: 0px;
113
+ border: 1px solid #000;
114
+ border-spacing: 0px;
115
+ height: 10px;
116
+ }
117
+
118
+ /* SYNTAX HIGHLIGHTING */
119
+
120
+ /* identifier */
121
+ .tI { color: #666; }
122
+
123
+ /* data type */
124
+ .tD { color: #cc3; font-style: italic; }
125
+
126
+ /* keyword */
127
+ .tK { color: #f30; }
128
+
129
+ /* comment */
130
+ .tC { color: #66f; font-style: italic; }
131
+
132
+ /* sql statement */
133
+ .tQ { color: #6c3; font-style: italic; }
134
+
135
+ /* string literal */
136
+ .tS { color: #390; }
137
+
138
+ /* label */
139
+ .tL { color: #630; font-style: italic; }
140
+
141
+ /* dollar quote marker */
142
+ .tM { }
143
+
144
+ /* tagged code blocks */
145
+ .b { display: block; width: 100%; margin: 0; padding: 0; }
146
+ .i { display: inline; }
147
+
148
+ /* line with incomplete coverage */
149
+ .lU
150
+ {
151
+ display: block;
152
+ background: #fdd;
153
+
154
+ margin: 0px;
155
+ padding: 0px;
156
+ border: none;
157
+ border-left: 2px solid #f00;
158
+ }
159
+
160
+ /* block execution: yes, no */
161
+ .c0 { font-weight: bold; }
162
+ .c1 { }
163
+
164
+ /* loop coverage */
165
+ .l0000 { font-weight: bold; }
166
+ .l0001 { font-weight: bold; }
167
+ .l0010 { font-weight: bold; }
168
+ .l0100 { font-weight: bold; }
169
+ .l0011 { font-weight: bold; }
170
+ .l0101 { font-weight: bold; }
171
+ .l0110 { font-weight: bold; }
172
+ .l0111 { font-weight: bold; }
173
+ .l1000 { font-weight: bold; }
174
+ .l1001 { font-weight: bold; }
175
+ .l1010 { font-weight: bold; }
176
+ .l1100 { font-weight: bold; }
177
+ .l1011 { font-weight: bold; }
178
+ .l1101 { font-weight: bold; }
179
+ .l1110 { font-weight: bold; }
180
+ .l1111 { }
181
+
182
+ /* branch decisions: true, false */
183
+ .b00 { font-weight: bold; } /* never evaluated */
184
+ .b01 { font-weight: bold; color: #060; } /* never evaluates true */
185
+ .b10 { font-weight: bold; color: #900; } /* never evaluates false */
186
+ .b11 { }
187
+