piggly 1.2.1 → 2.0.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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +163 -0
  3. data/Rakefile +29 -15
  4. data/bin/piggly +4 -244
  5. data/lib/piggly.rb +19 -17
  6. data/lib/piggly/command.rb +9 -0
  7. data/lib/piggly/command/base.rb +148 -0
  8. data/lib/piggly/command/report.rb +162 -0
  9. data/lib/piggly/command/test.rb +157 -0
  10. data/lib/piggly/command/trace.rb +90 -0
  11. data/lib/piggly/command/untrace.rb +78 -0
  12. data/lib/piggly/compiler.rb +7 -5
  13. data/lib/piggly/compiler/cache_dir.rb +119 -0
  14. data/lib/piggly/compiler/coverage_report.rb +63 -0
  15. data/lib/piggly/compiler/trace_compiler.rb +105 -0
  16. data/lib/piggly/config.rb +47 -22
  17. data/lib/piggly/dumper.rb +9 -0
  18. data/lib/piggly/dumper/index.rb +121 -0
  19. data/lib/piggly/dumper/qualified_name.rb +36 -0
  20. data/lib/piggly/dumper/qualified_type.rb +81 -0
  21. data/lib/piggly/dumper/reified_procedure.rb +142 -0
  22. data/lib/piggly/dumper/skeleton_procedure.rb +102 -0
  23. data/lib/piggly/installer.rb +84 -42
  24. data/lib/piggly/parser.rb +43 -49
  25. data/lib/piggly/parser/grammar.tt +289 -313
  26. data/lib/piggly/parser/nodes.rb +270 -211
  27. data/lib/piggly/parser/traversal.rb +35 -33
  28. data/lib/piggly/parser/treetop_ruby19_patch.rb +1 -1
  29. data/lib/piggly/profile.rb +81 -60
  30. data/lib/piggly/reporter.rb +5 -18
  31. data/lib/piggly/reporter/base.rb +103 -0
  32. data/lib/piggly/reporter/html_dsl.rb +63 -0
  33. data/lib/piggly/reporter/index.rb +108 -0
  34. data/lib/piggly/reporter/procedure.rb +104 -0
  35. data/lib/piggly/reporter/resources/highlight.js +21 -0
  36. data/lib/piggly/reporter/{piggly.css → resources/piggly.css} +52 -12
  37. data/lib/piggly/reporter/{sortable.js → resources/sortable.js} +0 -0
  38. data/lib/piggly/tags.rb +280 -0
  39. data/lib/piggly/task.rb +191 -40
  40. data/lib/piggly/util.rb +8 -27
  41. data/lib/piggly/util/blankslate.rb +114 -0
  42. data/lib/piggly/util/cacheable.rb +19 -0
  43. data/lib/piggly/util/enumerable.rb +44 -0
  44. data/lib/piggly/util/file.rb +17 -0
  45. data/lib/piggly/util/process_queue.rb +96 -0
  46. data/lib/piggly/util/thunk.rb +39 -0
  47. data/lib/piggly/version.rb +8 -8
  48. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  49. data/spec/examples/compiler/report_spec.rb +25 -0
  50. data/spec/{compiler → examples/compiler}/trace_spec.rb +7 -57
  51. data/spec/examples/config_spec.rb +61 -0
  52. data/spec/examples/dumper/index_spec.rb +197 -0
  53. data/spec/examples/dumper/procedure_spec.rb +116 -0
  54. data/spec/{grammar → examples/grammar}/expression_spec.rb +60 -60
  55. data/spec/{grammar → examples/grammar}/statements/assignment_spec.rb +15 -15
  56. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  57. data/spec/{grammar → examples/grammar}/statements/exception_spec.rb +10 -10
  58. data/spec/{grammar → examples/grammar}/statements/if_spec.rb +47 -34
  59. data/spec/{grammar → examples/grammar}/statements/loop_spec.rb +5 -5
  60. data/spec/{grammar → examples/grammar}/statements/sql_spec.rb +11 -11
  61. data/spec/{grammar → examples/grammar}/tokens/comment_spec.rb +11 -11
  62. data/spec/{grammar → examples/grammar}/tokens/datatype_spec.rb +14 -8
  63. data/spec/{grammar → examples/grammar}/tokens/identifier_spec.rb +26 -10
  64. data/spec/{grammar → examples/grammar}/tokens/keyword_spec.rb +5 -5
  65. data/spec/{grammar → examples/grammar}/tokens/label_spec.rb +7 -7
  66. data/spec/{grammar → examples/grammar}/tokens/literal_spec.rb +1 -1
  67. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  68. data/spec/{grammar → examples/grammar}/tokens/number_spec.rb +1 -1
  69. data/spec/{grammar → examples/grammar}/tokens/sqlkeywords_spec.rb +1 -1
  70. data/spec/{grammar → examples/grammar}/tokens/string_spec.rb +9 -9
  71. data/spec/{grammar → examples/grammar}/tokens/whitespace_spec.rb +1 -1
  72. data/spec/examples/installer_spec.rb +59 -0
  73. data/spec/examples/parser/nodes_spec.rb +73 -0
  74. data/spec/examples/parser/traversal_spec.rb +14 -0
  75. data/spec/examples/parser_spec.rb +115 -0
  76. data/spec/examples/profile_spec.rb +153 -0
  77. data/spec/{reporter/html_spec.rb → examples/reporter/html/dsl_spec.rb} +0 -0
  78. data/spec/examples/reporter/html/index_spec.rb +0 -0
  79. data/spec/examples/reporter/html_spec.rb +1 -0
  80. data/spec/examples/reporter_spec.rb +0 -0
  81. data/spec/{compiler → examples}/tags_spec.rb +10 -10
  82. data/spec/examples/task_spec.rb +0 -0
  83. data/spec/examples/util/cacheable_spec.rb +41 -0
  84. data/spec/examples/util/enumerable_spec.rb +64 -0
  85. data/spec/examples/util/file_spec.rb +40 -0
  86. data/spec/examples/util/process_queue_spec.rb +16 -0
  87. data/spec/examples/util/thunk_spec.rb +58 -0
  88. data/spec/examples/version_spec.rb +0 -0
  89. data/spec/issues/007_spec.rb +25 -0
  90. data/spec/issues/008_spec.rb +73 -0
  91. data/spec/issues/018_spec.rb +25 -0
  92. data/spec/spec_helper.rb +253 -9
  93. metadata +136 -93
  94. data/README.markdown +0 -116
  95. data/lib/piggly/compiler/cache.rb +0 -151
  96. data/lib/piggly/compiler/pretty.rb +0 -67
  97. data/lib/piggly/compiler/queue.rb +0 -46
  98. data/lib/piggly/compiler/tags.rb +0 -244
  99. data/lib/piggly/compiler/trace.rb +0 -91
  100. data/lib/piggly/filecache.rb +0 -40
  101. data/lib/piggly/parser/parser.rb +0 -11794
  102. data/lib/piggly/reporter/html.rb +0 -207
  103. data/spec/compiler/cache_spec.rb +0 -9
  104. data/spec/compiler/pretty_spec.rb +0 -9
  105. data/spec/compiler/queue_spec.rb +0 -3
  106. data/spec/compiler/rewrite_spec.rb +0 -3
  107. data/spec/config_spec.rb +0 -58
  108. data/spec/filecache_spec.rb +0 -70
  109. data/spec/fixtures/snippets.sql +0 -158
  110. data/spec/grammar/tokens/lval_spec.rb +0 -50
  111. data/spec/parser_spec.rb +0 -8
  112. data/spec/profile_spec.rb +0 -5
@@ -1,18 +1,20 @@
1
- unless defined?(PIGGLY_ROOT)
2
- PIGGLY_ROOT = File.join(File.dirname(__FILE__), 'piggly')
3
- end
4
-
5
- require 'fileutils'
6
- require 'digest/md5'
7
- require File.join(PIGGLY_ROOT, 'version')
8
- require File.join(PIGGLY_ROOT, 'config')
9
- require File.join(PIGGLY_ROOT, 'filecache')
10
- require File.join(PIGGLY_ROOT, 'compiler')
11
- require File.join(PIGGLY_ROOT, 'parser')
12
- require File.join(PIGGLY_ROOT, 'profile')
13
- require File.join(PIGGLY_ROOT, 'installer')
14
- require File.join(PIGGLY_ROOT, 'reporter')
15
- require File.join(PIGGLY_ROOT, 'util')
1
+ require "erb"
2
+ require "yaml"
3
+ require "optparse"
4
+ require "fileutils"
5
+ require "digest/md5"
6
+ require "set"
16
7
 
17
- # used to generate MD5 tags for AST nodes
18
- $PIGGLY_GENTAG = 0
8
+ module Piggly
9
+ autoload :VERSION, "piggly/version"
10
+ autoload :Config, "piggly/config"
11
+ autoload :Command, "piggly/command"
12
+ autoload :Compiler, "piggly/compiler"
13
+ autoload :Dumper, "piggly/dumper"
14
+ autoload :Parser, "piggly/parser"
15
+ autoload :Profile, "piggly/profile"
16
+ autoload :Installer, "piggly/installer"
17
+ autoload :Reporter, "piggly/reporter"
18
+ autoload :Tags, "piggly/tags"
19
+ autoload :Util, "piggly/util"
20
+ end
@@ -0,0 +1,9 @@
1
+ module Piggly
2
+ module Command
3
+ autoload :Base, "piggly/command/base"
4
+ autoload :Report, "piggly/command/report"
5
+ autoload :Test, "piggly/command/test"
6
+ autoload :Trace, "piggly/command/trace"
7
+ autoload :Untrace, "piggly/command/untrace"
8
+ end
9
+ end
@@ -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} {test|report|trace|untrace} --help"
14
+ else
15
+ cmd.main(argv)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # @return [(Class, Array<String>)]
22
+ def command(argv)
23
+ return if argv.empty?
24
+ head, *tail = argv
25
+
26
+ case head.downcase
27
+ when "report"; [Report, tail]
28
+ when "test"; [Test, tail]
29
+ when "trace"; [Trace, tail]
30
+ when "untrace"; [Untrace, tail]
31
+ end
32
+ end
33
+
34
+ # @return [PGconn]
35
+ def connect(config)
36
+ require "pg"
37
+ require "erb"
38
+
39
+ files = Array(config.database_yml ||
40
+ %w(piggly/database.yml
41
+ config/database.yml
42
+ piggly/database.json
43
+ config/database.json))
44
+
45
+ path = files.find{|x| File.exists?(x) } or
46
+ raise "No database config files found: #{files.join(", ")}"
47
+
48
+ specs =
49
+ if File.extname(path) == ".json"
50
+ require "json"
51
+ JSON.load(ERB.new(IO.read(path)).result)
52
+ else
53
+ require "yaml"
54
+ YAML.load(ERB.new(IO.read(path)).result)
55
+ end
56
+
57
+ spec = (specs.is_a?(Hash) and specs[config.connection_name]) or
58
+ raise "Database '#{config.connection_name}' is not configured in #{path}"
59
+
60
+ PGconn.connect(spec["host"], spec["port"], nil, nil,
61
+ spec["database"], spec["username"], spec["password"])
62
+ end
63
+
64
+ # @return [Enumerable<SkeletonProcedure>]
65
+ def filter(config, index)
66
+ if config.filters.empty?
67
+ index.procedures
68
+ else
69
+ head, _ = config.filters
70
+
71
+ start =
72
+ case head.first
73
+ when :+; []
74
+ when :-; index.procedures
75
+ end
76
+
77
+ config.filters.inject(start) do |s, pair|
78
+ case pair.first
79
+ when :+
80
+ s | index.procedures.select(&pair.last)
81
+ when :-
82
+ 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 { puts "piggly #{VERSION} #{VERSION::RELEASE_DATE}"; exit! }
114
+ end
115
+
116
+ def o_dry_run(config)
117
+ lambda { 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,157 @@
1
+ module Piggly
2
+ module Command
3
+
4
+ #
5
+ # This command handles all the setup and teardown for running Ruby tests, that can
6
+ # otherwise be accomplished in a more manual fashion, using the other commands. It
7
+ # assumes that the test files will automatically establish a connection to the correct
8
+ # database when they are loaded (this is the case for Rails).
9
+ #
10
+ module Test
11
+ class << self
12
+
13
+ def main(argv)
14
+ benchmark do
15
+ tests, filters = parse_options(argv)
16
+ profile = Profile.new
17
+
18
+ # don't let rspec get these when loading spec files
19
+ ARGV.clear
20
+
21
+ load_tests(tests)
22
+ Command.connect_to_database
23
+
24
+ procedures = dump_procedures(filters)
25
+
26
+ if procedures.empty?
27
+ abort "No stored procedures in the database#{' matched your criteria' if filters.any?}"
28
+ end
29
+
30
+ result =
31
+ begin
32
+ trace(procedures)
33
+ clear_coverage
34
+ execute_tests
35
+ ensure
36
+ untrace(procedures)
37
+ end
38
+
39
+ create_index(procedures)
40
+ create_reports(procedures)
41
+ store_coverage
42
+
43
+ exit! result # avoid running tests again
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def benchmark
50
+ start = Time.now
51
+ value = yield
52
+ puts " > Completed in #{'%0.2f' % (Time.now - start)} seconds"
53
+ return value
54
+ end
55
+
56
+ #
57
+ # Parses command-line options
58
+ #
59
+ def parse_options(argv)
60
+ filters = []
61
+
62
+ opts = OptionParser.new do |opts|
63
+ opts.on("-I", "--include PATHS", "Prepend paths to $LOAD_PATH (colon separated list)", &Command.method(:opt_include_path))
64
+ opts.on("-o", "--report-root PATH", "Report output directory", &Command.method(:opt_report_root))
65
+ opts.on("-c", "--cache-root PATH", "Local cache directory", &Command.method(:opt_cache_root))
66
+
67
+ opts.on("-n", "--name PATTERN", "Trace stored procedures matching PATTERN") do |opt|
68
+ if m = opt.match(%r{^/(.+)/$})
69
+ filters << lambda{|p| p.name.match(m.captures.first) }
70
+ else
71
+ filters << lambda{|p| p.name === opt }
72
+ end
73
+ end
74
+
75
+ opts.on("-a", "--aggregate", "Aggregate data from the previous run", &Command.method(:opt_aggregate))
76
+ opts.on("-V", "--version", "Show version", &Command.method(:opt_version))
77
+ opts.on("-h", "--help", "Show this message") do
78
+ puts opts
79
+ exit! 0
80
+ end
81
+ end
82
+
83
+ begin
84
+ opts.parse! argv
85
+ raise OptionParser::MissingArgument, "no tests specified" if argv.empty?
86
+ rescue OptionParser::InvalidOption,
87
+ OptionParser::InvalidArgument,
88
+ OptionParser::MissingArgument
89
+ puts opts
90
+ puts
91
+ puts $!
92
+
93
+ exit! 1
94
+ end
95
+
96
+ test_paths = argv.map{|p| Dir[p] }.flatten.sort
97
+
98
+ return test_paths, filters
99
+ end
100
+
101
+ def load_tests(tests)
102
+ puts "Loading #{tests.size} test files"
103
+ benchmark { tests.each{|file| load file }}
104
+ end
105
+
106
+ #
107
+ # Writes all stored procedures in the database to disk
108
+ #
109
+ def dump_procedures(filters)
110
+ Command::Trace.dump_procedures(filters)
111
+ end
112
+
113
+ #
114
+ # Compiles all the stored procedures on disk and installs them
115
+ #
116
+ def trace(procedures)
117
+ benchmark { Command::Trace.trace(procedures) }
118
+ end
119
+
120
+ def clear_coverage
121
+ Command::Report.clear_coverage
122
+ end
123
+
124
+ def execute_tests
125
+ if defined? ::Test::Unit::AutoRunner
126
+ ::Test::Unit::AutoRunner.run
127
+ elsif defined? ::RSpec::Core
128
+ ::Rspec::Core::Runner.run(ARGV, $stderr, $stdout)
129
+ elsif defined? ::Spec::Runner
130
+ ::Spec::Runner.run
131
+ elsif defined? ::MiniTest::Unit
132
+ ::MiniTest::Unit.new.run(ARGV)
133
+ else
134
+ raise "Neither Test::Unit, MiniTest, nor RSpec were detected"
135
+ end
136
+ end
137
+
138
+ def store_coverage(profile)
139
+ benchmark { Command::Report.store_coverage(profile) }
140
+ end
141
+
142
+ def untrace(procedures)
143
+ benchmark { Command::Untrace.untrace(procedures) }
144
+ end
145
+
146
+ def create_index(procedures, profile)
147
+ benchmark { Command::Report.create_index(procedures, profile) }
148
+ end
149
+
150
+ def create_reports(procedures, profile)
151
+ benchmark { Command::Report.create_reports(procedures, profile) }
152
+ end
153
+
154
+ end
155
+ end
156
+ end
157
+ end