piggly-nsd 2.3.3
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.
- checksums.yaml +7 -0
- data/README.md +170 -0
- data/Rakefile +33 -0
- data/bin/piggly +8 -0
- data/lib/piggly/command/base.rb +148 -0
- data/lib/piggly/command/report.rb +162 -0
- data/lib/piggly/command/trace.rb +90 -0
- data/lib/piggly/command/untrace.rb +78 -0
- data/lib/piggly/command.rb +8 -0
- data/lib/piggly/compiler/cache_dir.rb +119 -0
- data/lib/piggly/compiler/coverage_report.rb +63 -0
- data/lib/piggly/compiler/trace_compiler.rb +117 -0
- data/lib/piggly/compiler.rb +7 -0
- data/lib/piggly/config.rb +80 -0
- data/lib/piggly/dumper/index.rb +121 -0
- data/lib/piggly/dumper/qualified_name.rb +36 -0
- data/lib/piggly/dumper/qualified_type.rb +141 -0
- data/lib/piggly/dumper/reified_procedure.rb +172 -0
- data/lib/piggly/dumper/skeleton_procedure.rb +112 -0
- data/lib/piggly/dumper.rb +9 -0
- data/lib/piggly/installer.rb +137 -0
- data/lib/piggly/parser/grammar.tt +748 -0
- data/lib/piggly/parser/nodes.rb +378 -0
- data/lib/piggly/parser/traversal.rb +50 -0
- data/lib/piggly/parser/treetop_ruby19_patch.rb +21 -0
- data/lib/piggly/parser.rb +69 -0
- data/lib/piggly/profile.rb +108 -0
- data/lib/piggly/reporter/base.rb +106 -0
- data/lib/piggly/reporter/html_dsl.rb +63 -0
- data/lib/piggly/reporter/index.rb +114 -0
- data/lib/piggly/reporter/procedure.rb +129 -0
- data/lib/piggly/reporter/resources/highlight.js +38 -0
- data/lib/piggly/reporter/resources/piggly.css +515 -0
- data/lib/piggly/reporter/resources/sortable.js +493 -0
- data/lib/piggly/reporter.rb +8 -0
- data/lib/piggly/tags.rb +280 -0
- data/lib/piggly/task.rb +215 -0
- data/lib/piggly/util/blankslate.rb +114 -0
- data/lib/piggly/util/cacheable.rb +19 -0
- data/lib/piggly/util/enumerable.rb +44 -0
- data/lib/piggly/util/file.rb +17 -0
- data/lib/piggly/util/process_queue.rb +96 -0
- data/lib/piggly/util/thunk.rb +39 -0
- data/lib/piggly/util.rb +9 -0
- data/lib/piggly/version.rb +15 -0
- data/lib/piggly.rb +20 -0
- data/spec/examples/compiler/cacheable_spec.rb +190 -0
- data/spec/examples/compiler/report_spec.rb +25 -0
- data/spec/examples/compiler/trace_spec.rb +123 -0
- data/spec/examples/config_spec.rb +63 -0
- data/spec/examples/dumper/index_spec.rb +199 -0
- data/spec/examples/dumper/procedure_spec.rb +116 -0
- data/spec/examples/grammar/expression_spec.rb +302 -0
- data/spec/examples/grammar/statements/assignment_spec.rb +70 -0
- data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
- data/spec/examples/grammar/statements/exception_spec.rb +78 -0
- data/spec/examples/grammar/statements/if_spec.rb +191 -0
- data/spec/examples/grammar/statements/loop_spec.rb +41 -0
- data/spec/examples/grammar/statements/sql_spec.rb +71 -0
- data/spec/examples/grammar/tokens/comment_spec.rb +58 -0
- data/spec/examples/grammar/tokens/datatype_spec.rb +58 -0
- data/spec/examples/grammar/tokens/identifier_spec.rb +74 -0
- data/spec/examples/grammar/tokens/keyword_spec.rb +44 -0
- data/spec/examples/grammar/tokens/label_spec.rb +40 -0
- data/spec/examples/grammar/tokens/literal_spec.rb +30 -0
- data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
- data/spec/examples/grammar/tokens/number_spec.rb +34 -0
- data/spec/examples/grammar/tokens/sqlkeywords_spec.rb +45 -0
- data/spec/examples/grammar/tokens/string_spec.rb +54 -0
- data/spec/examples/grammar/tokens/whitespace_spec.rb +40 -0
- data/spec/examples/installer_spec.rb +59 -0
- data/spec/examples/parser/nodes_spec.rb +73 -0
- data/spec/examples/parser/traversal_spec.rb +14 -0
- data/spec/examples/parser_spec.rb +118 -0
- data/spec/examples/profile_spec.rb +153 -0
- data/spec/examples/reporter/html/dsl_spec.rb +0 -0
- data/spec/examples/reporter/html/index_spec.rb +0 -0
- data/spec/examples/reporter/html_spec.rb +1 -0
- data/spec/examples/reporter_spec.rb +0 -0
- data/spec/examples/tags_spec.rb +285 -0
- data/spec/examples/task_spec.rb +0 -0
- data/spec/examples/util/cacheable_spec.rb +41 -0
- data/spec/examples/util/enumerable_spec.rb +64 -0
- data/spec/examples/util/file_spec.rb +40 -0
- data/spec/examples/util/process_queue_spec.rb +16 -0
- data/spec/examples/util/thunk_spec.rb +59 -0
- data/spec/examples/version_spec.rb +0 -0
- data/spec/issues/007_spec.rb +25 -0
- data/spec/issues/008_spec.rb +73 -0
- data/spec/issues/018_spec.rb +25 -0
- data/spec/issues/028_spec.rb +48 -0
- data/spec/issues/032_spec.rb +98 -0
- data/spec/issues/036_spec.rb +41 -0
- data/spec/spec_helper.rb +312 -0
- data/spec/spec_suite.rb +5 -0
- metadata +162 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b53364d89c087bf4c2ce49ec451ece2cb1c17694bab9e765e034a05493ef3999
|
|
4
|
+
data.tar.gz: a46eea287041eeaa4130b730dd409f1f35663e6fca5258e96c639b1f567eeb11
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1cfd022ed8e03c56d372911943bd7b662a6ff967f63bde7b09a01e010b5117bc41ad0f5c245a30e72139776a5eece380ab9d2d60175462555a874cfb008e73c8
|
|
7
|
+
data.tar.gz: b8653bb06b92d31eee24e5c9a317fe912dc61e01d46fae649dadeb4610b831f4ad50199683996cddc756727124be68514fc0d919b372d80f70347851539d7751
|
data/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
[](https://github.com/sergeiboikov/piggly/actions/workflows/ci.yml)
|
|
2
|
+
# Piggly-NSD
|
|
3
|
+
|
|
4
|
+
Code coverage reports for PostgreSQL PL/pgSQL stored procedures
|
|
5
|
+
|
|
6
|
+
**Note:** This is a maintained fork of the original [piggly](https://github.com/kputnam/piggly) project by Kyle Putnam.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
PL/pgSQL doesn't have much in the way of developer tools, and writing automated tests for
|
|
13
|
+
stored procedures can be much easier when you know what you haven't tested. Code coverage
|
|
14
|
+
allows you to see which parts of your code haven't been executed.
|
|
15
|
+
|
|
16
|
+
Piggly is a tool (written in Ruby, but you can write your tests in any language) to track
|
|
17
|
+
code coverage of PostgreSQL PL/pgSQL stored procedures. It reports on code coverage to help
|
|
18
|
+
you identify untested parts of your code.
|
|
19
|
+
|
|
20
|
+
## How Does It Work?
|
|
21
|
+
|
|
22
|
+
Piggly tracks the execution of PostgreSQL's PL/pgSQL stored procedures by recompiling
|
|
23
|
+
the stored procedure with instrumentation code. Basically, RAISE WARNING statements notify the
|
|
24
|
+
client of an execution event (e.g., a branch condition evaluating to true or false). It records
|
|
25
|
+
these events and generates prettified source code that is annotated with coverage details.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
* Readable and easily-navigable reports (see [example](http://kputnam.github.io/piggly/reports/index.html))
|
|
30
|
+
* Language agnostic - write your tests in Ruby, Python, Java, SQL scripts etc
|
|
31
|
+
* Branch, block, and loop coverage analysis
|
|
32
|
+
* Instrumenting source-to-source compiler
|
|
33
|
+
* Low test execution overhead
|
|
34
|
+
* Reduced compilation times by use of disk caching
|
|
35
|
+
* Possible to aggregate coverage across multiple runs
|
|
36
|
+
|
|
37
|
+
## Limitations
|
|
38
|
+
|
|
39
|
+
* Not all PL/pgSQL grammar is currently supported, but the grammar is easy to modify
|
|
40
|
+
* Cannot parse nested dollar-quoted strings, eg $A$ ... $B$ ... $B$ ... $A$
|
|
41
|
+
* SQL statements are not instrumented, so their branches (COALESCE, WHERE-clauses, etc) aren't tracked
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
* [Treetop](http://github.com/nathansobo/treetop): `gem install treetop`
|
|
46
|
+
* The [ruby-pg driver](http://bitbucket.org/ged/ruby-pg/): `gem install pg`
|
|
47
|
+
* The examples require ActiveRecord: `gem install activerecord`
|
|
48
|
+
|
|
49
|
+
## Versioning
|
|
50
|
+
|
|
51
|
+
This fork continues the version numbering from the original piggly project:
|
|
52
|
+
- **2.3.1** - Last version of original piggly by Kyle Putnam
|
|
53
|
+
- **2.3.2** - NSD fork with UTF-8 encoding support and updated dependencies
|
|
54
|
+
|
|
55
|
+
## How to Install
|
|
56
|
+
|
|
57
|
+
To install the latest from github:
|
|
58
|
+
|
|
59
|
+
$ git clone https://github.com/sergeiboikov/piggly.git
|
|
60
|
+
$ cd piggly
|
|
61
|
+
$ bundle install
|
|
62
|
+
$ bundle exec rake spec
|
|
63
|
+
|
|
64
|
+
$ gem uninstall piggly-nsd # If piggly-nsd was installed earlier
|
|
65
|
+
$ gem build piggly.gemspec
|
|
66
|
+
$ gem install piggly-nsd-*.gem
|
|
67
|
+
|
|
68
|
+
To install the latest release:
|
|
69
|
+
|
|
70
|
+
$ gem install piggly-nsd
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Your stored procedures must already be loaded in the database. Configure your database connection in
|
|
75
|
+
a file named `config/database.yml` relative to where you want to run piggly. You can also specify the
|
|
76
|
+
`-d PATH` to an existing configuration file. The contents of the file follow ActiveRecord conventions:
|
|
77
|
+
|
|
78
|
+
piggly:
|
|
79
|
+
adapter: postgresql
|
|
80
|
+
database: cookbook
|
|
81
|
+
username: kputnam
|
|
82
|
+
password: secret
|
|
83
|
+
host: localhost
|
|
84
|
+
|
|
85
|
+
Here we'll add some stored procedures to the database we described above:
|
|
86
|
+
|
|
87
|
+
$ cat example/proc/*.sql | psql -U kputnam -h localhost cookbook
|
|
88
|
+
|
|
89
|
+
Note the connection is expected to be named `piggly` but you may specify the `-k DATABASE` option to
|
|
90
|
+
use a different connection name (eg `-k development` in Rails). See also `example/config/database.yml`.
|
|
91
|
+
|
|
92
|
+
Now you are ready to recompile and install your stored procedures. This reads the configuration from
|
|
93
|
+
`./config/database.yml` relative to the current working directory.
|
|
94
|
+
|
|
95
|
+
$ piggly trace
|
|
96
|
+
compiling 5 procedures
|
|
97
|
+
Compiling scramble
|
|
98
|
+
Compiling scramble
|
|
99
|
+
Compiling numberedargs
|
|
100
|
+
Compiling snippets
|
|
101
|
+
Compiling iterate
|
|
102
|
+
tracing 5 procedures
|
|
103
|
+
|
|
104
|
+
For running the development version use the following command:
|
|
105
|
+
`ruby bin/piggly trace`
|
|
106
|
+
|
|
107
|
+
This caches the original version (without instrumentation) in `piggly/cache` so you can restore them
|
|
108
|
+
later. Piggly will only recompile procedures that have changed in the database since it last
|
|
109
|
+
made a copy in `piggly/cache`.
|
|
110
|
+
|
|
111
|
+
*WARNING*: piggly fetches your code from the database and replaces it (in the database) with the
|
|
112
|
+
instrumented code. If you run `piggly trace` twice consecutively, the second time will cause an error
|
|
113
|
+
because you are trying to re-instrument code that has already been instrumented. You need to run
|
|
114
|
+
`piggly untrace` or restore your original stored procedures manually before you can trace them again.
|
|
115
|
+
|
|
116
|
+
Now you're ready to execute your tests. Make sure your connection is configured to log `RAISE WARNING`
|
|
117
|
+
messages to a file -- or you can log them to `STDERR` and redirect that to a file. For instance you
|
|
118
|
+
might run:
|
|
119
|
+
|
|
120
|
+
$ ant test 2> messages.txt
|
|
121
|
+
$ make test 2> messages.txt
|
|
122
|
+
etc.
|
|
123
|
+
|
|
124
|
+
To build the coverage report, have piggly read that file in by executing `piggly report < messages.txt`,
|
|
125
|
+
or `piggly report -f messages.txt`. You don't actually need the intermediate file, you can pipe your
|
|
126
|
+
test suite directly in like `ant test 2>&1 | piggly report`.
|
|
127
|
+
|
|
128
|
+
Once the report is built you can open it in `piggly/reports/index.html`.
|
|
129
|
+
|
|
130
|
+
## Running the Examples
|
|
131
|
+
|
|
132
|
+
$ cd piggly
|
|
133
|
+
$ bundle install
|
|
134
|
+
$ cat example/README
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
$ ./example/run-tests
|
|
138
|
+
compiling 5 procedures
|
|
139
|
+
Compiling scramble
|
|
140
|
+
Compiling scramble
|
|
141
|
+
Compiling numberedargs
|
|
142
|
+
Compiling snippets
|
|
143
|
+
Compiling iterate
|
|
144
|
+
tracing 5 procedures
|
|
145
|
+
Loaded suite /home/kputnam/wd/piggly/example/test/iterate_test
|
|
146
|
+
Started
|
|
147
|
+
......
|
|
148
|
+
Finished in 0.199236 seconds.
|
|
149
|
+
|
|
150
|
+
6 tests, 6 assertions, 0 failures, 0 errors, 0 skips
|
|
151
|
+
|
|
152
|
+
Test run options: --seed 25290
|
|
153
|
+
clearing previous coverage
|
|
154
|
+
storing coverage profile
|
|
155
|
+
creating index
|
|
156
|
+
creating reports
|
|
157
|
+
reporting coverage for scramble
|
|
158
|
+
reporting coverage for scramble
|
|
159
|
+
reporting coverage for numberedargs
|
|
160
|
+
reporting coverage for snippets
|
|
161
|
+
reporting coverage for iterate: +0.0% block, +0.0% branch, +0.0% loop
|
|
162
|
+
restoring 5 procedures
|
|
163
|
+
OK, view /home/kputnam/wd/piggly/example/piggly/reports/index.html
|
|
164
|
+
|
|
165
|
+
$ ls -alh example/reports/index.html
|
|
166
|
+
-rw-r--r-- 1 kputnam kputnam 1.4K 2010-04-28 11:21 example/reports/index.html
|
|
167
|
+
|
|
168
|
+
## Bugs & Issues
|
|
169
|
+
|
|
170
|
+
Please report any issues or feature requests on the [github tracker](https://github.com/sergeiboikov/piggly/issues).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require "rubygems"
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
rescue LoadError
|
|
5
|
+
warn "couldn't load bundler:"
|
|
6
|
+
warn " #{$!}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
begin # rspec-2
|
|
10
|
+
require "rspec/core/rake_task"
|
|
11
|
+
RSpec::Core::RakeTask.new do |t|
|
|
12
|
+
t.verbose = false
|
|
13
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
14
|
+
t.rspec_opts = "--color --format=p"
|
|
15
|
+
end
|
|
16
|
+
rescue LoadError => first
|
|
17
|
+
begin # rspec-1
|
|
18
|
+
require "spec/rake/spectask"
|
|
19
|
+
Spec::Rake::SpecTask.new do |t|
|
|
20
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
21
|
+
t.spec_opts << "--color"
|
|
22
|
+
t.spec_opts << "--format=p"
|
|
23
|
+
end
|
|
24
|
+
rescue LoadError => second
|
|
25
|
+
task :spec do
|
|
26
|
+
warn "couldn't load rspec version 1 or 2:"
|
|
27
|
+
warn " #{first}"
|
|
28
|
+
warn " #{second}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
task :default => :spec
|
data/bin/piggly
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Command
|
|
3
|
+
|
|
4
|
+
class Base
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class << Base
|
|
8
|
+
|
|
9
|
+
def main(argv)
|
|
10
|
+
cmd, argv = command(argv)
|
|
11
|
+
|
|
12
|
+
if cmd.nil?
|
|
13
|
+
abort "usage: #{$0} {report|trace|untrace} --help"
|
|
14
|
+
else
|
|
15
|
+
cmd.main(argv)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [(Class, Array<String>)]
|
|
20
|
+
def command(argv)
|
|
21
|
+
return if argv.empty?
|
|
22
|
+
head, *tail = argv
|
|
23
|
+
|
|
24
|
+
case head.downcase
|
|
25
|
+
when "report"; [Report, tail]
|
|
26
|
+
when "trace"; [Trace, tail]
|
|
27
|
+
when "untrace"; [Untrace, tail]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [PG::Connection]
|
|
32
|
+
def connect(config)
|
|
33
|
+
require "pg"
|
|
34
|
+
require "erb"
|
|
35
|
+
|
|
36
|
+
files = Array(config.database_yml ||
|
|
37
|
+
%w(piggly/database.yml
|
|
38
|
+
config/database.yml
|
|
39
|
+
piggly/database.json
|
|
40
|
+
config/database.json))
|
|
41
|
+
|
|
42
|
+
path = files.find{|x| File.exist?(x) } or
|
|
43
|
+
raise "No database config files found: #{files.join(", ")}"
|
|
44
|
+
|
|
45
|
+
specs =
|
|
46
|
+
if File.extname(path) == ".json"
|
|
47
|
+
require "json"
|
|
48
|
+
JSON.load(ERB.new(IO.read(path)).result)
|
|
49
|
+
else
|
|
50
|
+
require "yaml"
|
|
51
|
+
YAML.unsafe_load(ERB.new(IO.read(path)).result)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
spec = (specs.is_a?(Hash) and specs[config.connection_name]) or
|
|
55
|
+
raise "Database '#{config.connection_name}' is not configured in #{path}"
|
|
56
|
+
|
|
57
|
+
conn = PG::Connection.connect(spec["host"], spec["port"], nil, nil,
|
|
58
|
+
spec["database"], spec["username"], spec["password"])
|
|
59
|
+
|
|
60
|
+
# Set client encoding to UTF-8 to properly handle Cyrillic and other Unicode characters
|
|
61
|
+
conn.set_client_encoding('UTF8')
|
|
62
|
+
|
|
63
|
+
conn
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Enumerable<SkeletonProcedure>]
|
|
67
|
+
def filter(config, index)
|
|
68
|
+
if config.filters.empty?
|
|
69
|
+
index.procedures
|
|
70
|
+
else
|
|
71
|
+
head, _ = config.filters
|
|
72
|
+
|
|
73
|
+
start =
|
|
74
|
+
case head.first
|
|
75
|
+
when :+; []
|
|
76
|
+
when :-; index.procedures
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
config.filters.inject(start) do |s, pair|
|
|
80
|
+
case pair.first
|
|
81
|
+
when :+; s | index.procedures.select(&pair.last)
|
|
82
|
+
when :-; s.reject(&pair.last)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def o_accumulate(config)
|
|
89
|
+
lambda{|x| config.accumulate = x }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def o_cache_root(config)
|
|
93
|
+
lambda{|x| config.cache_root = x }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def o_report_root(config)
|
|
97
|
+
lambda{|x| config.report_root = x }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def o_include_paths(config)
|
|
101
|
+
lambda{|x| config.include_paths.concat(x.split(":")) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def o_database_yml(config)
|
|
105
|
+
lambda{|x| config.database_yml = x }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def o_connection_name(config)
|
|
109
|
+
lambda{|x| config.connection_name = x }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def o_version(config)
|
|
113
|
+
lambda {|x| puts "piggly #{VERSION} #{VERSION::RELEASE_DATE}"; exit! }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def o_dry_run(config)
|
|
117
|
+
lambda {|x| config.dry_run = true }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def o_select(config)
|
|
121
|
+
lambda do |x|
|
|
122
|
+
filter =
|
|
123
|
+
if m = x.match(%r{^/([^/]+)/$})
|
|
124
|
+
lambda{|p| p.name.to_s.match(m.captures.first) }
|
|
125
|
+
else
|
|
126
|
+
lambda{|p| p.name.to_s === x }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
config.filters << [:+, filter]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def o_reject(config)
|
|
134
|
+
lambda do |x|
|
|
135
|
+
filter =
|
|
136
|
+
if m = x.match(%r{^/([^/]+)/$})
|
|
137
|
+
lambda{|p| p.name.to_s.match(m.captures.first) }
|
|
138
|
+
else
|
|
139
|
+
lambda{|p| p.name.to_s === x }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
config.filters << [:-, filter]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Command
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This command reads a given file (or STDIN) which is expected to contain messages like the
|
|
6
|
+
# pattern Profile::PATTERN, which is probbaly "WARNING: PIGGLY 0123456789abcdef".
|
|
7
|
+
#
|
|
8
|
+
# Lines in the input that match this pattern are profiled and used to generate a report
|
|
9
|
+
#
|
|
10
|
+
class Report < Base
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class << Report
|
|
14
|
+
def main(argv)
|
|
15
|
+
require "pp"
|
|
16
|
+
io, config = configure(argv)
|
|
17
|
+
|
|
18
|
+
profile = Profile.new
|
|
19
|
+
index = Dumper::Index.new(config)
|
|
20
|
+
|
|
21
|
+
procedures = filter(config, index)
|
|
22
|
+
|
|
23
|
+
if procedures.empty?
|
|
24
|
+
if filters.empty?
|
|
25
|
+
abort "no stored procedures in the cache"
|
|
26
|
+
else
|
|
27
|
+
abort "no stored procedures in the cache matched your criteria"
|
|
28
|
+
end
|
|
29
|
+
elsif config.dry_run?
|
|
30
|
+
puts procedures.map{|p| p.signature }
|
|
31
|
+
exit 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
profile_procedures(config, procedures, profile)
|
|
35
|
+
clear_coverage(config, profile)
|
|
36
|
+
|
|
37
|
+
read_profile(config, io, profile)
|
|
38
|
+
store_coverage(profile)
|
|
39
|
+
|
|
40
|
+
create_index(config, index, procedures, profile)
|
|
41
|
+
create_reports(config, procedures, profile)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Adds the given procedures to Profile
|
|
45
|
+
#
|
|
46
|
+
def profile_procedures(config, procedures, profile)
|
|
47
|
+
# register each procedure in the Profile
|
|
48
|
+
compiler = Compiler::TraceCompiler.new(config)
|
|
49
|
+
procedures.each do |p|
|
|
50
|
+
result = compiler.compile(p)
|
|
51
|
+
profile.add(p, result[:tags], result)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear coverage after procedures have been loaded
|
|
56
|
+
#
|
|
57
|
+
def clear_coverage(config, profile)
|
|
58
|
+
unless config.accumulate?
|
|
59
|
+
puts "clearing previous coverage"
|
|
60
|
+
profile.clear
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reads +io+ for lines matching Profile::PATTERN and records coverage
|
|
65
|
+
#
|
|
66
|
+
def read_profile(config, io, profile)
|
|
67
|
+
np = profile.notice_processor(config)
|
|
68
|
+
io.each{|line| np.call(line) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Store the coverage Profile on disk
|
|
72
|
+
#
|
|
73
|
+
def store_coverage(profile)
|
|
74
|
+
puts "storing coverage profile"
|
|
75
|
+
profile.store
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create the report's index.html
|
|
79
|
+
#
|
|
80
|
+
def create_index(config, index, procedures, profile)
|
|
81
|
+
puts "creating index"
|
|
82
|
+
reporter = Reporter::Index.new(config, profile)
|
|
83
|
+
reporter.install("resources/piggly.css", "resources/sortable.js", "resources/highlight.js")
|
|
84
|
+
reporter.report(procedures, index)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Create each procedures' HTML report page
|
|
88
|
+
#
|
|
89
|
+
def create_reports(config, procedures, profile)
|
|
90
|
+
puts "creating reports"
|
|
91
|
+
queue = Util::ProcessQueue.new
|
|
92
|
+
|
|
93
|
+
compiler = Compiler::TraceCompiler.new(config)
|
|
94
|
+
reporter = Reporter::Procedure.new(config, profile)
|
|
95
|
+
|
|
96
|
+
Parser.parser
|
|
97
|
+
|
|
98
|
+
procedures.each do |p|
|
|
99
|
+
queue.add do
|
|
100
|
+
unless compiler.stale?(p)
|
|
101
|
+
data = compiler.compile(p)
|
|
102
|
+
path = reporter.report_path(p.source_path(config), ".html")
|
|
103
|
+
|
|
104
|
+
unless profile.empty?(data[:tags])
|
|
105
|
+
changes = ": #{profile.difference(p, data[:tags])}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
puts "reporting coverage for #{p.name}#{changes}"
|
|
109
|
+
# pp data[:tags]
|
|
110
|
+
# pp profile[p]
|
|
111
|
+
# puts
|
|
112
|
+
|
|
113
|
+
reporter.report(p)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
queue.execute
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def configure(argv, config = Config.new)
|
|
122
|
+
io = $stdin
|
|
123
|
+
p = OptionParser.new do |o|
|
|
124
|
+
o.on("-t", "--dry-run", "only print the names of matching procedures", &o_dry_run(config))
|
|
125
|
+
o.on("-s", "--select PATTERN", "select procedures matching PATTERN", &o_select(config))
|
|
126
|
+
o.on("-r", "--reject PATTERN", "ignore procedures matching PATTERN", &o_reject(config))
|
|
127
|
+
o.on("-c", "--cache-root PATH", "local cache directory", &o_cache_root(config))
|
|
128
|
+
o.on("-o", "--report-root PATH", "report output directory", &o_report_root(config))
|
|
129
|
+
o.on("-a", "--accumulate", "accumulate data from the previous run", &o_accumulate(config))
|
|
130
|
+
o.on("-V", "--version", "show version", &o_version(config))
|
|
131
|
+
o.on("-h", "--help", "show this message") { abort o.to_s }
|
|
132
|
+
o.on("-f", "--input PATH", "read trace messages from PATH") do |path|
|
|
133
|
+
io = if path == "-"
|
|
134
|
+
$stdin
|
|
135
|
+
else
|
|
136
|
+
File.open(path, "rb")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
begin
|
|
142
|
+
p.parse! argv
|
|
143
|
+
|
|
144
|
+
if io.eql?($stdin) and $stdin.tty?
|
|
145
|
+
raise OptionParser::MissingArgument,
|
|
146
|
+
"stdin must be a pipe, or use --input PATH"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return io, config
|
|
150
|
+
rescue OptionParser::InvalidOption,
|
|
151
|
+
OptionParser::InvalidArgument,
|
|
152
|
+
OptionParser::MissingArgument
|
|
153
|
+
puts p
|
|
154
|
+
puts
|
|
155
|
+
puts $!
|
|
156
|
+
exit! 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Command
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This command connects to the database, dumps all stored procedures, compiles them
|
|
6
|
+
# with instrumentation code, and finally installs the instrumented code.
|
|
7
|
+
#
|
|
8
|
+
class Trace < Base
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class << Trace
|
|
12
|
+
def main(argv)
|
|
13
|
+
config = configure(argv)
|
|
14
|
+
connection = connect(config)
|
|
15
|
+
index = Dumper::Index.new(config)
|
|
16
|
+
|
|
17
|
+
dump(connection, index)
|
|
18
|
+
|
|
19
|
+
procedures = filter(config, index)
|
|
20
|
+
|
|
21
|
+
if procedures.empty?
|
|
22
|
+
if config.filters.empty?
|
|
23
|
+
abort "no stored procedures in the cache"
|
|
24
|
+
else
|
|
25
|
+
abort "no stored procedures in the cache matched your criteria"
|
|
26
|
+
end
|
|
27
|
+
elsif config.dry_run?
|
|
28
|
+
puts procedures.map{|p| p.signature }
|
|
29
|
+
exit 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
trace(config, procedures)
|
|
33
|
+
install(Installer.new(config, connection), procedures, Profile.new)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Writes all stored procedures in the database to disk
|
|
37
|
+
# @return [void]
|
|
38
|
+
def dump(connection, index)
|
|
39
|
+
index.update(Dumper::ReifiedProcedure.all(connection))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Compiles all the stored procedures on disk and installs them
|
|
43
|
+
# @return [void]
|
|
44
|
+
def trace(config, procedures)
|
|
45
|
+
puts "compiling #{procedures.size} procedures"
|
|
46
|
+
|
|
47
|
+
compiler = Compiler::TraceCompiler.new(config)
|
|
48
|
+
queue = Util::ProcessQueue.new
|
|
49
|
+
procedures.each{|p| queue.add { compiler.compile(p) }}
|
|
50
|
+
|
|
51
|
+
# Force parser to load before we start forking
|
|
52
|
+
Parser.parser
|
|
53
|
+
queue.execute
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def install(installer, procedures, profile)
|
|
57
|
+
puts "tracing #{procedures.size} procedures"
|
|
58
|
+
installer.install(procedures, profile)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parses command-line options
|
|
62
|
+
# @return [Config]
|
|
63
|
+
def configure(argv, config = Config.new)
|
|
64
|
+
p = OptionParser.new do |o|
|
|
65
|
+
o.on("-t", "--dry-run", "only print the names of matching procedures", &o_dry_run(config))
|
|
66
|
+
o.on("-s", "--select PATTERN", "select procedures matching PATTERN", &o_select(config))
|
|
67
|
+
o.on("-r", "--reject PATTERN", "ignore procedures matching PATTERN", &o_reject(config))
|
|
68
|
+
o.on("-c", "--cache-root PATH", "local cache directory", &o_cache_root(config))
|
|
69
|
+
o.on("-o", "--report-root PATH", "report output directory", &o_report_root(config))
|
|
70
|
+
o.on("-d", "--database PATH", "read database adapter settings from YAML/JSON file", &o_database_yml(config))
|
|
71
|
+
o.on("-k", "--connection NAME", "use connection adapter NAME", &o_connection_name(config))
|
|
72
|
+
o.on("-V", "--version", "show version", &o_version(config))
|
|
73
|
+
o.on("-h", "--help", "show this message") { abort o.to_s }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
p.parse! argv
|
|
78
|
+
config
|
|
79
|
+
rescue OptionParser::InvalidOption,
|
|
80
|
+
OptionParser::InvalidArgument,
|
|
81
|
+
OptionParser::MissingArgument
|
|
82
|
+
puts p
|
|
83
|
+
puts
|
|
84
|
+
puts $!
|
|
85
|
+
exit! 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Piggly
|
|
2
|
+
module Command
|
|
3
|
+
|
|
4
|
+
class Untrace < Base
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class << Untrace
|
|
8
|
+
def main(argv)
|
|
9
|
+
config = configure(argv)
|
|
10
|
+
index = Dumper::Index.new(config)
|
|
11
|
+
connection = connect(config)
|
|
12
|
+
procedures = filter(config, index)
|
|
13
|
+
|
|
14
|
+
if procedures.empty?
|
|
15
|
+
if config.filters.empty?
|
|
16
|
+
abort "no stored procedures in the cache"
|
|
17
|
+
else
|
|
18
|
+
abort "no stored procedures in the cache matched your criteria"
|
|
19
|
+
end
|
|
20
|
+
elsif config.dry_run?
|
|
21
|
+
puts procedures.map{|x| x.signature }
|
|
22
|
+
exit 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
untrace(Installer.new(config, connection), procedures)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Restores database procedures from file cache
|
|
30
|
+
#
|
|
31
|
+
def untrace(installer, procedures)
|
|
32
|
+
puts "restoring #{procedures.size} procedures"
|
|
33
|
+
installer.uninstall(procedures)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Returns a list of Procedure values that satisfy at least one of the given filters
|
|
38
|
+
#
|
|
39
|
+
def find_procedures(filters, index)
|
|
40
|
+
if filters.empty?
|
|
41
|
+
index.procedures
|
|
42
|
+
else
|
|
43
|
+
filters.inject(Set.new){|set, filter| set | index.procedures.select(&filter) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# Parses command-line options
|
|
49
|
+
#
|
|
50
|
+
def configure(argv, config = Config.new)
|
|
51
|
+
p = OptionParser.new do |o|
|
|
52
|
+
o.on("-t", "--dry-run", "only print the names of matching procedures", &o_dry_run(config))
|
|
53
|
+
o.on("-s", "--select PATTERN", "select procedures matching PATTERN", &o_select(config))
|
|
54
|
+
o.on("-r", "--reject PATTERN", "ignore procedures matching PATTERN", &o_reject(config))
|
|
55
|
+
o.on("-c", "--cache-root PATH", "local cache directory", &o_cache_root(config))
|
|
56
|
+
o.on("-d", "--database PATH", "read 'piggly' database adapter settings from YAML file", &o_database_yml(config))
|
|
57
|
+
o.on("-k", "--connection NAME", "use connection adapter NAME", &o_connection_name(config))
|
|
58
|
+
o.on("-V", "--version", "show version", &o_version(config))
|
|
59
|
+
o.on("-h", "--help", "show this message") { abort o.to_s }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
p.parse! argv
|
|
64
|
+
config
|
|
65
|
+
rescue OptionParser::InvalidOption,
|
|
66
|
+
OptionParser::InvalidArgument,
|
|
67
|
+
OptionParser::MissingArgument
|
|
68
|
+
puts p
|
|
69
|
+
puts
|
|
70
|
+
puts $!
|
|
71
|
+
exit! 1
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end
|
|
78
|
+
end
|