piggly 1.2.0

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