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.
- data/README.markdown +84 -0
- data/Rakefile +19 -0
- data/bin/piggly +245 -0
- data/lib/piggly/compiler/cache.rb +151 -0
- data/lib/piggly/compiler/pretty.rb +67 -0
- data/lib/piggly/compiler/queue.rb +46 -0
- data/lib/piggly/compiler/tags.rb +244 -0
- data/lib/piggly/compiler/trace.rb +91 -0
- data/lib/piggly/compiler.rb +5 -0
- data/lib/piggly/config.rb +43 -0
- data/lib/piggly/filecache.rb +40 -0
- data/lib/piggly/installer.rb +95 -0
- data/lib/piggly/parser/grammar.tt +747 -0
- data/lib/piggly/parser/nodes.rb +319 -0
- data/lib/piggly/parser/parser.rb +11783 -0
- data/lib/piggly/parser/traversal.rb +48 -0
- data/lib/piggly/parser/treetop_ruby19_patch.rb +17 -0
- data/lib/piggly/parser.rb +67 -0
- data/lib/piggly/profile.rb +87 -0
- data/lib/piggly/reporter/html.rb +207 -0
- data/lib/piggly/reporter/piggly.css +187 -0
- data/lib/piggly/reporter/sortable.js +493 -0
- data/lib/piggly/reporter.rb +21 -0
- data/lib/piggly/task.rb +64 -0
- data/lib/piggly/util.rb +28 -0
- data/lib/piggly/version.rb +15 -0
- data/lib/piggly.rb +18 -0
- data/spec/compiler/cache_spec.rb +9 -0
- data/spec/compiler/pretty_spec.rb +9 -0
- data/spec/compiler/queue_spec.rb +3 -0
- data/spec/compiler/rewrite_spec.rb +3 -0
- data/spec/compiler/tags_spec.rb +285 -0
- data/spec/compiler/trace_spec.rb +173 -0
- data/spec/config_spec.rb +58 -0
- data/spec/filecache_spec.rb +70 -0
- data/spec/fixtures/snippets.sql +158 -0
- data/spec/grammar/expression_spec.rb +302 -0
- data/spec/grammar/statements/assignment_spec.rb +70 -0
- data/spec/grammar/statements/exception_spec.rb +52 -0
- data/spec/grammar/statements/if_spec.rb +178 -0
- data/spec/grammar/statements/loop_spec.rb +41 -0
- data/spec/grammar/statements/sql_spec.rb +71 -0
- data/spec/grammar/tokens/comment_spec.rb +58 -0
- data/spec/grammar/tokens/datatype_spec.rb +52 -0
- data/spec/grammar/tokens/identifier_spec.rb +58 -0
- data/spec/grammar/tokens/keyword_spec.rb +44 -0
- data/spec/grammar/tokens/label_spec.rb +40 -0
- data/spec/grammar/tokens/literal_spec.rb +30 -0
- data/spec/grammar/tokens/lval_spec.rb +50 -0
- data/spec/grammar/tokens/number_spec.rb +34 -0
- data/spec/grammar/tokens/sqlkeywords_spec.rb +45 -0
- data/spec/grammar/tokens/string_spec.rb +54 -0
- data/spec/grammar/tokens/whitespace_spec.rb +40 -0
- data/spec/parser_spec.rb +8 -0
- data/spec/profile_spec.rb +5 -0
- data/spec/reporter/html_spec.rb +0 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/spec_suite.rb +5 -0
- 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
|