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
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