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
data/README.markdown ADDED
@@ -0,0 +1,84 @@
1
+ # Piggly - PostgreSQL PL/pgSQL stored procedure code coverage
2
+
3
+ ## What's Piggly?
4
+ Piggly is like [RCov] [1] for PostgreSQL's PL/pgSQL stored procedures. It reports on code
5
+ coverage to help you identify untested parts of your code.
6
+
7
+ ## Features
8
+ * Branch, block, and loop coverage analysis
9
+ * Instrumenting source-to-source compiler
10
+ * Low test execution overhead
11
+ * Reduced compilation times by use of disk caching
12
+ * Readable and easily navigable reports (see example/piggly/reports/index.html)
13
+ * Able to aggregate coverage across multiple runs
14
+ * Test::Unit and RSpec compatible
15
+
16
+ ## Limitations
17
+ * Cannot parse aggregate definitions (but helper functions are fine)
18
+ * Cannot parse nested dollar-quoted strings, eg $A$ ... $B$ ... $B$ ... $A$
19
+ * Report generation is resource intensive and slow
20
+
21
+ ## Requirements
22
+ * [Treetop] [2]
23
+ * Stored procedures stored on the filesystem, defined with "CREATE OR REPLACE FUNCTION ..."
24
+ * The [ruby-pg driver] [3], and for the time being, ActiveRecord (some workaround should be possible)
25
+
26
+ ## Usage
27
+ Assume your stored procedures are in proc/, and the tests that should be exercising your
28
+ stored procedures are in spec/.
29
+
30
+ $ cd piggly/example/
31
+ $ ../bin/piggly -s 'proc/*.sql' 'spec/**/*_spec.rb'
32
+ Loading 1 test files
33
+ > Completed in 0.30 seconds
34
+ Compiling 1 files
35
+ Compiling /home/kputnam/wd/piggly/example/proc/iterate.sql
36
+ > Completed in 0.09 seconds
37
+ Installing 1 proc files
38
+ > Completed in 0.02 seconds
39
+ Clearing previous run's profile
40
+ > Completed in 0.00 seconds
41
+ ...........
42
+
43
+ Finished in 0.025061 seconds
44
+
45
+ 11 examples, 0 failures
46
+ Storing coverage profile
47
+ > Completed in 0.00 seconds
48
+ Removing trace code
49
+ > Completed in 0.00 seconds
50
+ Creating index
51
+ > Completed in 0.00 seconds
52
+ Creating reports
53
+ > Completed in 0.02 seconds
54
+ > Completed in 0.65 seconds
55
+
56
+ $ ls -alh piggly/reports/index.html
57
+ -rw-r--r-- 1 kputnam kputnam 1.3K 2010-04-19 14:25 piggly/reports/index.html
58
+
59
+ Note the compilation can be slow on the first run, but on subsequent runs it shouldn't need
60
+ to be compiled again. If a file is added or changed (based on mtime), it will be recompiled.
61
+
62
+ Piggly can also be run from Rake, with a task like:
63
+
64
+ namespace :spec do
65
+ Piggly::Task.new(:piggly => 'db:test:prepare') do |t|
66
+ t.libs.push 'spec'
67
+
68
+ t.test_files = FileList['spec/**/*_spec.rb']
69
+ t.proc_files = FileList['procs/*.sql']
70
+
71
+ # this can be used if piggly is frozen in a Rails application
72
+ t.libs.concat Dir['vendor/gems/*/lib/'].sort.reverse
73
+ t.piggly_path = Dir['vendor/gems/piggly-*/bin/piggly'].sort.last
74
+ end
75
+ end
76
+
77
+ $ rake spec:piggly
78
+
79
+ ## Author
80
+ * Kyle Putnam <putnam.kyle@gmail.com>
81
+
82
+ [1]: http://github.com/relevance/rcov/
83
+ [2]: http://github.com/nathansobo/treetop
84
+ [3]: http://bitbucket.org/ged/ruby-pg/
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ dir = File.dirname(__FILE__)
2
+ require 'rubygems'
3
+ require 'rake'
4
+
5
+ begin
6
+ require 'spec/rake/spectask'
7
+ Spec::Rake::SpecTask.new do |t|
8
+ t.pattern = 'spec/**/*_spec.rb'
9
+ end
10
+ task :default => :spec
11
+ rescue Exception
12
+ end
13
+
14
+ require 'rake/gempackagetask'
15
+ load './piggly.gemspec'
16
+ Rake::GemPackageTask.new(Piggly.gemspec) do |pkg|
17
+ pkg.need_tar = false
18
+ pkg.need_zip = false
19
+ end
data/bin/piggly ADDED
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'rubygems'
4
+ $:.push File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'piggly'
6
+
7
+ # Number of children to fork for parallel tasks
8
+ Piggly::Queue.children = 2
9
+
10
+ STDOUT.sync = true
11
+
12
+ module Piggly
13
+ module Command
14
+ class << self
15
+
16
+ def main
17
+ benchmark do
18
+ sources, tests = parse_options
19
+ load_tests(tests)
20
+ connect_to_database
21
+ compile_procs(sources)
22
+ install_procs(sources)
23
+ clear_coverage
24
+ execute_tests
25
+ store_coverage
26
+ uninstall_procs(sources)
27
+ create_index(sources)
28
+ create_reports(sources)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def benchmark
35
+ start = Time.now
36
+ yield
37
+ puts " > Completed in #{'%0.2f' % (Time.now - start)} seconds"
38
+ end
39
+
40
+ def parse_options
41
+ proc_paths = []
42
+
43
+ opts = OptionParser.new do |opts|
44
+ opts.on("-I", "--include PATHS", "Prepend paths to $: (colon separated list)") do |paths|
45
+ $:.concat paths.split(':')
46
+ end
47
+
48
+ opts.on("-o", "--output PATH", "Report output directory") do |dir|
49
+ Piggly::Config.report_root = dir
50
+ end
51
+
52
+ opts.on("-c", "--cache-root PATH", "Compiler cache directory") do |dir|
53
+ Piggly::Config.cache_root = dir
54
+ end
55
+
56
+ opts.on("-s", "--proc-files PATH", "Stored procedures file list (may be specified many times)") do |dir|
57
+ proc_paths << dir
58
+ end
59
+
60
+ # opts.on("-T", "--trace-prefix PATH", "Trace prefix") do |str|
61
+ # Piggly::Config.trace_prefix = str
62
+ # end
63
+
64
+ opts.on("-a", "--aggregate", "Aggregate data from the previous run") do
65
+ Piggly::Config.aggregate = true
66
+ end
67
+
68
+ opts.on("--version", "Show version") do
69
+ puts "piggly #{Piggly::VERSION::STRING} #{Piggly::VERSION::RELEASE_DATE}"
70
+ exit
71
+ end
72
+
73
+ opts.on("-h", "--help", "Show this message") do
74
+ puts opts
75
+ exit 0
76
+ end
77
+ end
78
+
79
+ if index = ARGV.index('--')
80
+ extra = ARGV.slice!(index..-1).slice(1..-1)
81
+ else
82
+ extra = []
83
+ end
84
+
85
+ begin
86
+ opts.parse! ARGV
87
+ raise OptionParser::MissingArgument, "no tests specified" if ARGV.empty?
88
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument, OptionParser::MissingArgument
89
+ puts opts
90
+ puts
91
+ puts $!.message
92
+ exit -1
93
+ end
94
+
95
+ proc_paths = proc_paths.map{|p| Dir[p] }.flatten.sort
96
+ test_paths = ARGV.map{|p| Dir[p] }.flatten.sort
97
+
98
+ ARGV.clear
99
+
100
+ return proc_paths, test_paths
101
+ end
102
+
103
+ def load_tests(tests)
104
+ puts "Loading #{tests.size} test files"
105
+
106
+ benchmark { tests.each{|file| load file } }
107
+
108
+ # # TODO: this doesn't seem right, but workaround RSpec executing each spec twice
109
+ # if defined? Spec::Runner::ExampleGroupRunner
110
+ # Spec::Runner::ExampleGroupRunner.send(:define_method, :load_files) do |*args|
111
+ # # don't do anything, we already loaded the files
112
+ # end
113
+ # end
114
+ end
115
+
116
+ def connect_to_database
117
+ ActiveRecord::Base.connection.active?
118
+ rescue
119
+ ActiveRecord::Base.establish_connection
120
+ end
121
+
122
+ def compile_procs(sources)
123
+ stale = sources.select{|f| Piggly::TraceCompiler.stale?(f) }
124
+
125
+ # build the parser if needed
126
+ Piggly::Parser.parser
127
+
128
+ puts "Compiling #{stale.size} files"
129
+ benchmark do
130
+ stale.each do |file|
131
+ Piggly::Queue.child do
132
+ begin
133
+ Piggly::TraceCompiler.cache(file)
134
+ # rescue Errno::ENOENT, Piggly::Parser::Failure
135
+ # puts "! #{File.basename file}"
136
+ rescue
137
+ puts
138
+ puts "#{$!.class}: #{$!.message}"
139
+ puts $!.backtrace.join("\n")
140
+ end
141
+ end
142
+ end
143
+ Piggly::Queue.start
144
+ end
145
+ end
146
+
147
+ def install_procs(sources)
148
+ puts "Installing #{sources.size} proc files"
149
+
150
+ benchmark do
151
+ sources.each do |file|
152
+ begin
153
+ Piggly::Installer.trace_proc(file)
154
+ rescue Errno::ENOENT, Piggly::Parser::Failure
155
+ # puts "! #{File.basename file}"
156
+ rescue
157
+ puts
158
+ puts "#{$!.class}: #{$!.message}"
159
+ puts $!.backtrace.join("\n")
160
+ end
161
+ end
162
+
163
+ Piggly::Installer.install_trace
164
+ end
165
+ end
166
+
167
+ def clear_coverage
168
+ unless Piggly::Config.aggregate
169
+ puts "Clearing previous run's profile"
170
+ benchmark { Piggly::Profile.clear }
171
+ end
172
+ end
173
+
174
+ def execute_tests
175
+ if defined? Spec::Runner
176
+ Spec::Runner.run
177
+ else
178
+ Test::Unit::AutoRunner.run
179
+ end
180
+ end
181
+
182
+ def store_coverage
183
+ puts "Storing coverage profile"
184
+ benchmark { Piggly::Profile.store }
185
+ end
186
+
187
+ def uninstall_procs(sources)
188
+ puts "Removing trace code"
189
+ benchmark do
190
+ sources.each do |file|
191
+ begin
192
+ Piggly::Installer.untrace_proc(file)
193
+ rescue Errno::ENOENT, Piggly::Parser::Failure
194
+ # puts "! #{File.basename file}"
195
+ rescue
196
+ puts
197
+ puts "#{$!.class}: #{$!.message}"
198
+ puts $!.backtrace.join("\n")
199
+ end
200
+ end
201
+
202
+ Piggly::Installer.uninstall_trace
203
+ end
204
+ end
205
+
206
+ def create_index(sources)
207
+ puts "Creating index"
208
+ benchmark do
209
+ Piggly::Reporter.install('piggly.css', 'sortable.js')
210
+ Piggly::HtmlReporter::Index.output(sources)
211
+ end
212
+ end
213
+
214
+ def create_reports(sources)
215
+ puts "Creating reports"
216
+ summary = Hash.new{|h,k| h[k] = Hash.new[:count => 0, :percent => 0]}
217
+
218
+ benchmark do
219
+ sources.each do |file|
220
+ Piggly::Queue.child do
221
+ begin
222
+ summary = Piggly::Profile.summary(file)
223
+ pretty = Piggly::PrettyCompiler.compile(file, Piggly::Profile)
224
+
225
+ Piggly::HtmlReporter.output(file, pretty, summary)
226
+ rescue Errno::ENOENT, Piggly::Parser::Failure
227
+ # puts "! #{File.basename file}"
228
+ rescue
229
+ puts "#{$!.class}: #{$!.message}"
230
+ puts $!.backtrace.join("\n")
231
+ end
232
+ end
233
+ end
234
+
235
+ Piggly::Queue.start
236
+ end
237
+ end
238
+
239
+ end
240
+ end
241
+ end
242
+
243
+ if __FILE__ == $0
244
+ Piggly::Command.main
245
+ end
@@ -0,0 +1,151 @@
1
+ module Piggly
2
+ module CompilerCache
3
+ def self.included(subclass)
4
+ subclass.extend(ClassMethods)
5
+ end
6
+
7
+ #
8
+ # Each cache unit (any group of data that should be expired and created
9
+ # together) can be broken apart, to prevent unmarshaling a huge block of
10
+ # data all at once.
11
+ #
12
+ # The interface works like a Hash, so the compile method should return a
13
+ # hash of objects. Each object is writen to a different file (named by the
14
+ # hash key) within the same directory. String objects are (usually) read
15
+ # and written directly to disk, while all other objects are (un-)Marshal'd
16
+ #
17
+ # Cache invalidation is done by comparing mtime timestamps on the cached
18
+ # object's file to all the "source" files (ruby libs, input files, etc)
19
+ # required to regenerate the data.
20
+ #
21
+ class FileCache
22
+ # Non-printable ASCII char indicates data should be Marshal'd
23
+ HINT = /[\000-\010\016-\037\177-\300]/
24
+
25
+ class << self
26
+ def lookup(cachedir, data={})
27
+ store[cachedir] ||= new(cachedir, data)
28
+ end
29
+
30
+ def dump(cachedir, hash)
31
+ FileUtils.mkdir(cachedir) unless File.exists?(cachedir)
32
+ FileUtils.touch(cachedir)
33
+
34
+ for key, data in hash
35
+ File.open(File.join(cachedir, key.to_s), 'wb') do |f|
36
+ if data.is_a?(String) and data[0,2] !~ HINT
37
+ # even Strings will be Marshal'd if the first two bytes contain non-ASCII
38
+ f.write data
39
+ else
40
+ Marshal.dump(data, f)
41
+ end
42
+ end
43
+ end
44
+
45
+ return hash
46
+ end
47
+
48
+ def load(cachedir, key)
49
+ File.open(File.join(cachedir, key.to_s)) do |io|
50
+ # detect Marshal'd data
51
+ if io.read(2) !~ HINT
52
+ io.rewind
53
+ io.read
54
+ else
55
+ io.rewind
56
+ Marshal.load(io)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Creates cachedir (if missing) and destroys its contents
62
+ def clean(cachedir)
63
+ FileUtils.mkdir(cachedir) unless File.exists?(cachedir)
64
+ FileUtils.touch(cachedir)
65
+ FileUtils.rm(Dir["#{cachedir}/*"])
66
+ end
67
+
68
+ def store
69
+ @store ||= {}
70
+ end
71
+ end
72
+
73
+ # Destroys any existing cached data and write the given data to disk if data is not empty
74
+ def initialize(cachedir, data={})
75
+ @dir = cachedir
76
+ @data = {}
77
+ replace(data) unless data.empty?
78
+ end
79
+
80
+ # Load +key+ from file system if needed
81
+ def [](key)
82
+ @data[key.to_s] ||= self.class.load(@dir, key)
83
+ end
84
+
85
+ # Writes through to file system
86
+ def []=(key, value)
87
+ @data[key.to_s] = value
88
+ self.class.dump(@dir, key.to_s => value)
89
+ end
90
+
91
+ # Writes through to file system
92
+ def update(hash)
93
+ hash.each do |key,value|
94
+ self[key] = value
95
+ end
96
+ end
97
+
98
+ def keys
99
+ Dir[@dir + '/*'].map{|e| File.basename(e) } | @data.keys
100
+ end
101
+
102
+ private
103
+
104
+ # Clears entire cache and replaces contents
105
+ def replace(data)
106
+ self.class.clean(@dir)
107
+ self.class.dump(@dir, data)
108
+
109
+ for key, value in data
110
+ # stringify keys
111
+ @data[key.to_s] = data
112
+ end
113
+ end
114
+ end
115
+
116
+ #
117
+ # Base class should define self.compiler_path and self.compile(tree, ...)
118
+ #
119
+ module ClassMethods
120
+ def cache_sources
121
+ [compiler_path, Parser.grammar_path, Parser.parser_path, Parser.nodes_path]
122
+ end
123
+
124
+ def stale?(source)
125
+ File.stale?(cache_path(source), source, *cache_sources)
126
+ end
127
+
128
+ # returns FileCache instance
129
+ def cache(source, args={}, &block)
130
+ Parser.parser # load libraries
131
+ cachedir = cache_path(source)
132
+
133
+ if stale?(source)
134
+ begin
135
+ tree = Parser.cache(source)
136
+ data = compile(tree, args.update(:path => source), &block)
137
+
138
+ # replaces old cached data with new data
139
+ FileCache.lookup(cachedir, data)
140
+ rescue Piggly::Parser::Failure
141
+ FileCache.clean(cachedir)
142
+ raise
143
+ end
144
+ else
145
+ FileCache.lookup(cachedir)
146
+ end
147
+ end
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), *%w(.. reporter))
2
+
3
+ module Piggly
4
+
5
+ #
6
+ # Produces HTML output to report coverage of tagged nodes in the tree
7
+ #
8
+ class PrettyCompiler
9
+ include Piggly::HtmlTag
10
+
11
+ def self.compile(path, profile)
12
+ new(profile).send(:compile, path)
13
+ end
14
+
15
+ def initialize(profile)
16
+ @profile = profile
17
+ end
18
+
19
+ private
20
+
21
+ def compile(path)
22
+ lines = File.read(path).count("\n") + 1
23
+
24
+ # recompile (should be cache hit) to identify tagged nodes
25
+ data = TraceCompiler.cache(path)
26
+ html = traverse(data['tree'])
27
+
28
+ return 'html' => html,
29
+ 'lines' => 1..lines,
30
+ 'tags' => data['tags']
31
+ end
32
+
33
+ def traverse(node, string='')
34
+ if node.terminal?
35
+ # terminals (leaves) are never tagged
36
+ if node.style
37
+ string << '<span class="' << node.style << '">' << e(node.text_value) << '</span>'
38
+ else
39
+ string << e(node.text_value)
40
+ end
41
+ else
42
+ # non-terminals never write their text_value
43
+ node.elements.each do |child|
44
+ if child.tagged?
45
+
46
+ # retreive the profiled tag
47
+ tag = @profile.by_id[child.tag_id]
48
+
49
+ if tag.complete?
50
+ string << '<span class="' << tag.style << '" id="T' << tag.id << '">'
51
+ else
52
+ string << '<span class="' << tag.style << '" id="T' << tag.id << '" title="' << tag.description << '">'
53
+ end
54
+
55
+ traverse(child, string)
56
+ string << '</span>'
57
+ else
58
+ traverse(child, string)
59
+ end
60
+ end
61
+ end
62
+
63
+ string
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ module Piggly
2
+
3
+ #
4
+ # Executes blocks in parallel subprocesses
5
+ #
6
+ class Queue
7
+
8
+ def self.children=(value)
9
+ @children = value
10
+ end
11
+
12
+ # add a compile job to the queue
13
+ def self.queue(&block)
14
+ (@queue ||= []) << block
15
+ end
16
+
17
+ def self.child
18
+ queue { yield }
19
+ end
20
+
21
+ # start scheduler thread
22
+ def self.start
23
+ @active = 0
24
+ @children ||= 1
25
+ @queue ||= []
26
+
27
+ while block = @queue.shift
28
+ if @active >= @children
29
+ pid = Process.wait
30
+ @active -= 1
31
+ end
32
+
33
+ # enable enterprise ruby feature
34
+ GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
35
+
36
+ # use exit! to avoid auto-running any test suites
37
+ pid = Process.fork{ block.call; exit! 0 }
38
+
39
+ @active += 1
40
+ end
41
+
42
+ Process.waitall
43
+ end
44
+
45
+ end
46
+ end