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,244 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Coverage is tracked by attaching these compiler-generated tags to various nodes in a stored
5
+ # procedure's parse tree. These tags each have a unique string identifier which is printed by
6
+ # various parts of the recompiled stored procedure, and the output is then recognized by
7
+ # Profile.notice_processor, which calls #ping on the tag corresponding to the printed string.
8
+ #
9
+ # After test execution is complete, each AST is walked and Tag values attached to NodeClass
10
+ # values are used to produce the coverage report
11
+ #
12
+ class Tag
13
+ PATTERN = /[0-9a-f]{16}/
14
+
15
+ attr_accessor :id
16
+
17
+ def initialize(prefix = nil, id = nil)
18
+ @id = Digest::MD5.hexdigest(prefix.to_s + (id || object_id).to_s).slice(0, 16)
19
+ end
20
+
21
+ alias to_s id
22
+
23
+ # Defined here in case ActiveSupport hasn't defined it on Object
24
+ def tap
25
+ yield self
26
+ self
27
+ end
28
+ end
29
+
30
+ class EvaluationTag < Tag
31
+ def initialize(*args)
32
+ clear
33
+ super
34
+ end
35
+
36
+ def type
37
+ :block
38
+ end
39
+
40
+ def ping(value)
41
+ @ran = true
42
+ end
43
+
44
+ def style
45
+ "c#{@ran ? '1' : '0'}"
46
+ end
47
+
48
+ def to_f
49
+ @ran ? 100.0 : 0.0
50
+ end
51
+
52
+ def complete?
53
+ @ran
54
+ end
55
+
56
+ def description
57
+ @ran ? 'full coverage' : 'never evaluated'
58
+ end
59
+
60
+ # Resets code coverage
61
+ def clear
62
+ @ran = false
63
+ end
64
+ end
65
+
66
+ #
67
+ # Sequence of statements
68
+ #
69
+ class BlockTag < EvaluationTag
70
+ end
71
+
72
+ #
73
+ # Procedure calls, raise exception, exits, returns
74
+ #
75
+ class UnconditionalBranchTag < EvaluationTag
76
+ # aggregate this coverage data with conditional branches
77
+ def type
78
+ :branch
79
+ end
80
+ end
81
+
82
+ class LoopConditionTag < Tag
83
+ STATES = { # never terminates normally (so @pass must be false)
84
+ 0b0000 => 'condition was never evaluated',
85
+ 0b0001 => 'condition never evaluates false (terminates early). loop always iterates more than once',
86
+ 0b0010 => 'condition never evaluates false (terminates early). loop always iterates only once',
87
+ 0b0011 => 'condition never evaluates false (terminates early)',
88
+ # terminates normally (one of @pass, @once, @twice must be true)
89
+ 0b1001 => 'loop always iterates more than once',
90
+ 0b1010 => 'loop always iterates only once',
91
+ 0b1011 => 'loop never passes through',
92
+ 0b1100 => 'loop always passes through',
93
+ 0b1101 => 'loop never iterates only once',
94
+ 0b1110 => 'loop never iterates more than once',
95
+ 0b1111 => 'full coverage' }
96
+
97
+ attr_reader :pass, :once, :twice, :ends, :count
98
+
99
+ def initialize(*args)
100
+ clear
101
+ super
102
+ end
103
+
104
+ def type
105
+ :loop
106
+ end
107
+
108
+ def ping(value)
109
+ case value
110
+ when 't'
111
+ # loop iterated
112
+ @count += 1
113
+ else
114
+ # loop terminated
115
+ case @count
116
+ when 0; @pass = true
117
+ when 1; @once = true
118
+ else; @twice = true
119
+ end
120
+ @count = 0
121
+
122
+ # this isn't accurate. there needs to be a signal at the end
123
+ # of the loop body to indicate it was reached. otherwise its
124
+ # possible each iteration restarts early with 'continue'
125
+ @ends = true
126
+ end
127
+ end
128
+
129
+ def style
130
+ "l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}}"
131
+ end
132
+
133
+ def to_f
134
+ # value space:
135
+ # (1,2,X) - loop iterated at least twice and terminated normally
136
+ # (1,X) - loop iterated only once and terminated normally
137
+ # (0,X) - loop never iterated and terminated normally (pass-thru)
138
+ # () - loop condition was never executed
139
+ #
140
+ # these combinations are ignored, because adding tests for them will probably not reveal bugs
141
+ # (1,2) - loop iterated at least twice but terminated early
142
+ # (1) - loop iterated only once but terminated early
143
+ 100 * ([@pass, @once, @twice, @ends].count{|x| x } / 4.0)
144
+ end
145
+
146
+ def complete?
147
+ @pass and @once and @twice and @ends
148
+ end
149
+
150
+ def description
151
+ # weird hack so ForCollectionTag uses its separate constant
152
+ self.class::STATES.fetch(n = state, "unknown tag state: #{n}")
153
+ end
154
+
155
+ # Returns state represented as a 4-bit integer
156
+ def state
157
+ [@ends,@pass,@once,@twice].reverse.inject([0,0]){|(k,n), bit| [k + 1, n | (bit ? 1 : 0) << k] }.last
158
+ end
159
+
160
+ def clear
161
+ @pass = false
162
+ @once = false
163
+ @twice = false
164
+ @ends = false
165
+ @count = 0
166
+ end
167
+ end
168
+
169
+ class ForCollectionTag < LoopConditionTag
170
+ STATES = LoopConditionTag::STATES.merge \
171
+ 0b0001 => 'loop always iterates more than once and always terminates early.',
172
+ 0b0010 => 'loop always iterates only once and always terminates early.',
173
+ 0b0011 => 'loop always terminates early',
174
+ 0b0100 => 'loop always passes through'
175
+
176
+ def ping(value)
177
+ case value
178
+ when 't'
179
+ # start of iteration
180
+ @count += 1
181
+ when '@'
182
+ # end of iteration
183
+ @ends = true
184
+ when 'f'
185
+ # loop exit
186
+ case @count
187
+ when 0; @pass = true
188
+ when 1; @once = true
189
+ else; @twice = true
190
+ end
191
+ @count = 0
192
+ end
193
+ end
194
+ end
195
+
196
+ class BranchConditionTag < Tag
197
+ attr_reader :true, :false
198
+
199
+ def initialize(*args)
200
+ clear
201
+ super
202
+ end
203
+
204
+ def type
205
+ :branch
206
+ end
207
+
208
+ def ping(value)
209
+ case value
210
+ when 't'; @true = true
211
+ when 'f'; @false = true
212
+ end
213
+ end
214
+
215
+ def style
216
+ "b#{@true ? 1 : 0}#{@false ? 1 : 0 }"
217
+ end
218
+
219
+ def to_f
220
+ (@true and @false) ? 100.0 : (@true or @false) ? 50.0 : 0.0
221
+ end
222
+
223
+ def complete?
224
+ @true and @false
225
+ end
226
+
227
+ def description
228
+ if @true and @false
229
+ 'full coverage'
230
+ elsif @true
231
+ 'never evaluates false'
232
+ elsif @false
233
+ 'never evaluates true'
234
+ else
235
+ 'never evaluated'
236
+ end
237
+ end
238
+
239
+ def clear
240
+ @true, @false = false
241
+ end
242
+ end
243
+
244
+ end
@@ -0,0 +1,91 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Walks the parse tree, attaching Tag values and rewriting source code to ping them.
5
+ #
6
+ class TraceCompiler
7
+ include FileCache
8
+ include CompilerCache
9
+
10
+ attr_accessor :nodes
11
+
12
+ def self.compile(tree, args)
13
+ new(args.fetch(:path)).send(:compile, tree)
14
+ end
15
+
16
+ def self.compiler_path
17
+ __FILE__
18
+ end
19
+
20
+ def initialize(path)
21
+ # create unique prefix for each file, prepended to each node's tag
22
+ @prefix = File.expand_path(path)
23
+ @tags = []
24
+ end
25
+
26
+ #
27
+ # Destructively modifies +tree+ (by attaching tags) and returns the tree
28
+ # along with the modified source code, and the list of tags. The tag list
29
+ # is passed along to Profile to compute coverage information. The tree is
30
+ # passed to PrettyCompiler
31
+ #
32
+ def compile(tree)
33
+ puts "Compiling #{@prefix}"
34
+ return 'code.sql' => traverse(tree),
35
+ 'tree' => tree,
36
+ 'tags' => @tags,
37
+ 'prefix' => @prefix
38
+ end
39
+
40
+ def traverse(node)
41
+ if node.terminal? or node.expression?
42
+ node.source_text
43
+ else
44
+ if node.respond_to?(:condStub) and node.respond_to?(:cond)
45
+ # preserve opening parenthesis and whitespace before injecting code. this way
46
+ # IF(test) becomes IF(piggly_cond(TAG, test)) instead of IFpiggly_cond(TAG, (test))
47
+ pre, cond = node.cond.expr.text_value.match(/\A(\(?[\t\n\r ]*)(.+)\z/m).captures
48
+ node.cond.source_text = ""
49
+
50
+ @tags << node.cond.tag(@prefix)
51
+
52
+ node.condStub.source_text = "#{pre}piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, #{cond})"
53
+ node.condStub.source_text << traverse(node.cond.tail) # preserve trailing whitespace
54
+ end
55
+
56
+ if node.respond_to?(:bodyStub)
57
+ if node.respond_to?(:exitStub) and node.respond_to?(:cond)
58
+ @tags << node.body.tag(@prefix)
59
+ @tags << node.cond.tag(@prefix)
60
+
61
+ # a hack to simulate a loop conditional statement in ForLoop. signal condition was true
62
+ # when body is executed. when exit stub is reached, signal condition was false
63
+ node.bodyStub.source_text = "perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, true);#{node.indent(:bodySpace)}"
64
+ node.bodyStub.source_text << "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
65
+
66
+ if node.respond_to?(:doneStub)
67
+ # signal the end of an iteration was reached
68
+ node.doneStub.source_text = "#{node.indent(:bodySpace)}perform piggly_signal($PIGGLY$#{node.cond.tag_id}$PIGGLY$, $PIGGLY$@$PIGGLY$);"
69
+ node.doneStub.source_text << node.body.indent
70
+ end
71
+
72
+ # signal the loop terminated
73
+ node.exitStub.source_text = "\n#{node.indent}perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, false);"
74
+ elsif node.respond_to?(:body)
75
+ # no condition:
76
+ # BEGIN ... END;
77
+ # LOOP ... END;
78
+ # ... ELSE ... END;
79
+ # CONTINUE label;
80
+ # EXIT label;
81
+ @tags << node.body.tag(@prefix)
82
+ node.bodyStub.source_text = "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
83
+ end
84
+ end
85
+
86
+ # traverse children (in which we just injected code)
87
+ node.elements.map{|e| traverse(e) }.join
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,5 @@
1
+ require File.join(File.dirname(__FILE__), *%w[compiler cache])
2
+ require File.join(File.dirname(__FILE__), *%w[compiler tags])
3
+ require File.join(File.dirname(__FILE__), *%w[compiler trace])
4
+ require File.join(File.dirname(__FILE__), *%w[compiler pretty])
5
+ require File.join(File.dirname(__FILE__), *%w[compiler queue])
@@ -0,0 +1,43 @@
1
+ module Piggly
2
+ class Config
3
+
4
+ def self.config_accessor(hash)
5
+ hash.keys.each do |name|
6
+ self.class.send(:define_method, name) do
7
+ instance_variable_get("@#{name}") || hash[name]
8
+ end
9
+ self.class.send(:define_method, "#{name}=") do |value|
10
+ instance_variable_set("@#{name}", value)
11
+ end
12
+ end
13
+ end
14
+
15
+ config_accessor :cache_root => File.expand_path(File.join(Dir.pwd, 'piggly', 'cache')),
16
+ :report_root => File.expand_path(File.join(Dir.pwd, 'piggly', 'reports')),
17
+ :piggly_root => PIGGLY_ROOT,
18
+ :trace_prefix => 'PIGGLY',
19
+ :aggregate => false
20
+
21
+ def self.path(root, file=nil)
22
+ if file
23
+ file[%r{^\.\.|^\/|^(?:[A-Z]:)?/}i] ?
24
+ file : # ../path, /path, or D:\path that isn't relative to root
25
+ File.join(root, file)
26
+ else
27
+ root
28
+ end
29
+ end
30
+
31
+ def self.mkpath(root, file=nil)
32
+ if file.nil?
33
+ FileUtils.makedirs(root)
34
+ root
35
+ else
36
+ path = path(root, file)
37
+ FileUtils.makedirs(File.dirname(path))
38
+ path
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ class File
2
+
3
+ # True if target file is older (by mtime) than any source file
4
+ def self.stale?(target, *sources)
5
+ if exists?(target)
6
+ oldest = mtime(target)
7
+ sources.any?{|x| mtime(x) > oldest }
8
+ else
9
+ true
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ module Piggly
16
+ module FileCache
17
+ def self.included(subclass)
18
+ subclass.extend(ClassMethods)
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ # Maps source path to cache path, like /home/user/foo.sql => piggly/cache/#{MD5('/home/user')}/#{BaseClass}/foo.sql
24
+ def cache_path(file)
25
+ # up to the last capitalized word of the class name
26
+ subdir = name[/^(?:.+::)?(.+?)([A-Z][^A-Z]+)?$/, 1]
27
+ root = File.join(Config.cache_root, subdir)
28
+
29
+ # md5 the full path to prevent collisions
30
+ full = File.expand_path(file)
31
+ base = File.basename(full)
32
+ hash = Digest::MD5.hexdigest(File.dirname(full))
33
+
34
+ Config.mkpath(File.join(Config.cache_root, hash, subdir), base)
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,95 @@
1
+ module Piggly
2
+ class Installer
3
+
4
+ # Compiles the procedures in +file+ with instrumentation and installs them
5
+ def self.trace_proc(file)
6
+ # recompile with instrumentation if needed
7
+ cache = Piggly::TraceCompiler.cache(file)
8
+
9
+ # install instrumented code
10
+ connection.exec cache['code.sql']
11
+
12
+ # map tag messages to tag objects
13
+ Profile.add(file, cache['tags'], cache)
14
+ end
15
+
16
+ # Reinstalls the original stored procedures in +file+
17
+ def self.untrace_proc(file)
18
+ connection.exec File.read(file)
19
+ end
20
+
21
+ # Installs necessary instrumentation support
22
+ def self.install_trace
23
+ # record trace messages
24
+ connection.set_notice_processor(&Profile.notice_processor)
25
+
26
+ # install tracing functions
27
+ connection.exec <<-SQL
28
+ -- signals that a conditional expression was executed
29
+ CREATE OR REPLACE FUNCTION piggly_cond(message varchar, value boolean)
30
+ RETURNS boolean AS $$
31
+ BEGIN
32
+ IF value THEN
33
+ RAISE WARNING '#{Config.trace_prefix} % t', message;
34
+ ELSE
35
+ RAISE WARNING '#{Config.trace_prefix} % f', message;
36
+ END IF;
37
+ RETURN value;
38
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
39
+ SQL
40
+
41
+ connection.exec <<-SQL
42
+ -- generic signal
43
+ CREATE OR REPLACE FUNCTION piggly_signal(message varchar, signal varchar)
44
+ RETURNS void AS $$
45
+ BEGIN
46
+ RAISE WARNING '#{Config.trace_prefix} % %', message, signal;
47
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
48
+ SQL
49
+
50
+ connection.exec <<-SQL
51
+ -- signals that a (sub)expression was executed. handles '' and NULL value
52
+ CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value varchar)
53
+ RETURNS varchar AS $$
54
+ BEGIN
55
+ RAISE WARNING '#{Config.trace_prefix} %', message;
56
+ RETURN value;
57
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
58
+ SQL
59
+
60
+ connection.exec <<-SQL
61
+ -- signals that a (sub)expression was executed. handles all other types
62
+ CREATE OR REPLACE FUNCTION piggly_expr(message varchar, value anyelement)
63
+ RETURNS anyelement AS $$
64
+ BEGIN
65
+ RAISE WARNING '#{Config.trace_prefix} %', message;
66
+ RETURN value;
67
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
68
+ SQL
69
+
70
+ connection.exec <<-SQL
71
+ -- signals that a branch was taken
72
+ CREATE OR REPLACE FUNCTION piggly_branch(message varchar)
73
+ RETURNS void AS $$
74
+ BEGIN
75
+ RAISE WARNING '#{Config.trace_prefix} %', message;
76
+ END $$ LANGUAGE 'plpgsql' VOLATILE;
77
+ SQL
78
+ end
79
+
80
+ # Uninstalls instrumentation support
81
+ def self.uninstall_trace
82
+ connection.set_notice_processor
83
+ connection.exec "DROP FUNCTION IF EXISTS piggly_cond(varchar, boolean);"
84
+ connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, varchar);"
85
+ connection.exec "DROP FUNCTION IF EXISTS piggly_expr(varchar, anyelement);"
86
+ connection.exec "DROP FUNCTION IF EXISTS piggly_branch(varchar);"
87
+ end
88
+
89
+ # Returns the active PGConn
90
+ def self.connection
91
+ ActiveRecord::Base.connection.raw_connection
92
+ end
93
+
94
+ end
95
+ end