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
@@ -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
@@ -1,5 +1,7 @@
1
- require File.join(File.dirname(__FILE__), *%w[compiler cache])
2
- require File.join(File.dirname(__FILE__), *%w[compiler tags])
3
- require File.join(File.dirname(__FILE__), *%w[compiler trace])
4
- require File.join(File.dirname(__FILE__), *%w[compiler pretty])
5
- require File.join(File.dirname(__FILE__), *%w[compiler queue])
1
+ module Piggly
2
+ module Compiler
3
+ autoload :CacheDir, "piggly/compiler/cache_dir"
4
+ autoload :TraceCompiler, "piggly/compiler/trace_compiler"
5
+ autoload :CoverageReport, "piggly/compiler/coverage_report"
6
+ end
7
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: ascii-8bit
2
+
3
+ module Piggly
4
+ module Compiler
5
+
6
+ #
7
+ # Each cache unit (any group of data that should be expired and created
8
+ # together) can be broken apart, to prevent unmarshaling a huge block of
9
+ # data all at once.
10
+ #
11
+ # The interface works like a Hash, so the compile method should return a
12
+ # hash of objects. Each object is writen to a different file (named by the
13
+ # hash key) within the same directory. String objects are (usually) read
14
+ # and written directly to disk, while all other objects are (un-)Marshal'd
15
+ #
16
+ # Cache invalidation is done by comparing mtime timestamps on the cached
17
+ # object's file to all the "source" files (ruby libs, input files, etc)
18
+ # required to regenerate the data.
19
+ #
20
+ class CacheDir
21
+ # Non-printable ASCII char indicates data should be Marshal'd
22
+ HINT = /[\000-\010\016-\037\177-\300]/
23
+
24
+ def initialize(dir)
25
+ @dir = dir
26
+ @data = Hash.new do |h, k|
27
+ path = File.join(@dir, k.to_s)
28
+ if File.exists?(path)
29
+ h[k.to_s] = File.open(path, "rb") do |io|
30
+ # Detect Marshal'd data
31
+ if io.read(2) !~ HINT
32
+ io.rewind
33
+ io.read
34
+ else
35
+ io.rewind
36
+ Marshal.load(io)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # Load given key from file system into memory if needed
44
+ # @return [Object]
45
+ def [](key)
46
+ @data[key.to_s]
47
+ end
48
+
49
+ # Writes through to file system
50
+ # @return [void]
51
+ def []=(key, value)
52
+ @data[key.to_s] = value
53
+ write(key.to_s => value)
54
+ end
55
+
56
+ # Writes through to file system and returns self
57
+ # @return [CacheDir] self
58
+ def update(hash)
59
+ hash.each{|k,v| self[k] = v }
60
+ self
61
+ end
62
+
63
+ # @return [void]
64
+ def delete(key)
65
+ path = File.join(@dir, key.to_s)
66
+ File.unlink(path) if File.exists?(path)
67
+ @data.delete(key)
68
+ end
69
+
70
+ # @return [Array<String>]
71
+ def keys
72
+ Dir[@dir + "/*"].map{|f| File.basename(f) }
73
+ end
74
+
75
+ # Creates cachedir, destroys its contents, and returns self
76
+ # @return [CacheDir] self
77
+ def clear
78
+ @data.clear
79
+
80
+ if File.exists?(@dir)
81
+ FileUtils.rm(Dir["#{@dir}/*"])
82
+ FileUtils.touch(@dir)
83
+ else
84
+ FileUtils.mkdir(@dir)
85
+ end
86
+
87
+ self
88
+ end
89
+
90
+ # Clears entire cache, replaces contents, and returns self
91
+ # @return [CacheDir] self
92
+ def replace(hash)
93
+ clear
94
+ update(hash)
95
+ end
96
+
97
+ private
98
+
99
+ # Serializes each entry to disk
100
+ # @return [void]
101
+ def write(hash)
102
+ FileUtils.mkdir(@dir) unless File.exists?(@dir)
103
+ FileUtils.touch(@dir) # Update mtime
104
+
105
+ hash.each do |key, value|
106
+ File.open(File.join(@dir, key.to_s), "wb") do |io|
107
+ # Marshal if the first two bytes contain non-ASCII
108
+ if value.is_a?(String) and value[0,2] !~ HINT
109
+ io.write(value)
110
+ else
111
+ Marshal.dump(value, io)
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,63 @@
1
+ module Piggly
2
+ module Compiler
3
+
4
+ #
5
+ # Produces HTML output to report coverage of tagged nodes in the tree
6
+ #
7
+ class CoverageReport
8
+ include Reporter::HtmlDsl
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def compile(procedure, profile)
15
+ trace = Compiler::TraceCompiler.new(@config)
16
+
17
+ if trace.stale?(procedure)
18
+ raise StaleCacheError,
19
+ "stale cached syntax tree for #{procedure.name}"
20
+ end
21
+
22
+ # Get (copies of) the tagged nodes from the compiled tree
23
+ data = trace.compile(procedure)
24
+
25
+ return :html => traverse(data[:tree], profile),
26
+ :lines => 1 .. procedure.source(@config).count("\n") + 1
27
+ end
28
+
29
+ protected
30
+
31
+ # @return [String]
32
+ def traverse(node, profile, string = "")
33
+ if node.tagged?
34
+ tag = profile[node.tag_id]
35
+
36
+ if tag.complete?
37
+ string << %[<span class="#{tag.style}" id="T#{tag.id}">]
38
+ else
39
+ string << %[<span class="#{tag.style}" id="T#{tag.id}" title="#{tag.description}">]
40
+ end
41
+ end
42
+
43
+ if node.terminal?
44
+ if style = node.style
45
+ string << %[<span class="#{style}">#{e(node.text_value)}</span>]
46
+ else
47
+ string << e(node.text_value)
48
+ end
49
+ else
50
+ # Non-terminals never write their text_value
51
+ node.elements.each{|child| traverse(child, profile, string) }
52
+ end
53
+
54
+ if node.tagged?
55
+ string << %[</span>]
56
+ end
57
+
58
+ string
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,105 @@
1
+ module Piggly
2
+ module Compiler
3
+
4
+ #
5
+ # Walks the parse tree, attaching Tag values and rewriting source code to ping them.
6
+ #
7
+ class TraceCompiler
8
+ include Util::Cacheable
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Is the cache_path is older than its source path or the other files?
15
+ def stale?(procedure)
16
+ Util::File.stale?(cache_path(procedure.source_path(@config)),
17
+ procedure.source_path(@config),
18
+ *self.class.cache_sources)
19
+ end
20
+
21
+ def compile(procedure)
22
+ cache = CacheDir.new(cache_path(procedure.source_path(@config)))
23
+
24
+ if stale?(procedure)
25
+ $stdout.puts "Compiling #{procedure.name}"
26
+ tree = Parser.parse(IO.read(procedure.source_path(@config)))
27
+ tree = tree.force! if tree.respond_to?(:thunk?)
28
+
29
+ tags = []
30
+ code = traverse(tree, procedure.oid, tags)
31
+
32
+ cache.replace(:tree => tree, :code => code, :tags => tags)
33
+ end
34
+
35
+ cache
36
+ end
37
+
38
+ protected
39
+
40
+ # Rewrites the parse tree to call instrumentation helpers, and destructively
41
+ # updates `tags` by appending the tags of instrumented nodes
42
+ # @return [String]
43
+ def traverse(node, oid, tags)
44
+ if node.terminal? or node.expression?
45
+ node.source_text
46
+ else
47
+ if node.respond_to?(:condStub) and node.respond_to?(:cond)
48
+ # Preserve opening parenthesis and whitespace before injecting code. This way
49
+ # IF(test) becomes IF(piggly_cond(TAG, test)) instead of IFpiggly_cond(TAG, (test))
50
+ pre, cond = node.cond.expr.text_value.match(/\A(\(?[\t\n\r ]*)(.+)\z/m).captures
51
+ node.cond.source_text = ""
52
+
53
+ tags << node.cond.tag(oid)
54
+
55
+ node.condStub.source_text = "#{pre}piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, (#{cond}))"
56
+ node.condStub.source_text << traverse(node.cond.tail, oid, tags) # preserve trailing whitespace
57
+ end
58
+
59
+ if node.respond_to?(:bodyStub)
60
+ if node.respond_to?(:exitStub) and node.respond_to?(:cond)
61
+ tags << node.body.tag(oid)
62
+ tags << node.cond.tag(oid)
63
+
64
+ # Hack to simulate a loop conditional statement in stmtForLoop and stmtLoop.
65
+ # signal condition is true when body is executed, and false when exit stub is reached
66
+ node.bodyStub.source_text = "perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, true);#{node.indent(:bodySpace)}"
67
+ node.bodyStub.source_text << "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
68
+
69
+ if node.respond_to?(:doneStub)
70
+ # Signal the end of an iteration was reached
71
+ node.doneStub.source_text = "#{node.indent(:bodySpace)}perform piggly_signal($PIGGLY$#{node.cond.tag_id}$PIGGLY$, $PIGGLY$@$PIGGLY$);"
72
+ node.doneStub.source_text << node.body.indent
73
+ end
74
+
75
+ # Signal the loop terminated
76
+ node.exitStub.source_text = "\n#{node.indent}perform piggly_cond($PIGGLY$#{node.cond.tag_id}$PIGGLY$, false);"
77
+ elsif node.respond_to?(:body)
78
+ # Unconditional branches (or blocks)
79
+ # BEGIN ... END;
80
+ # ... ELSE ... END;
81
+ # CONTINUE label;
82
+ # EXIT label;
83
+ tags << node.body.tag(oid)
84
+ node.bodyStub.source_text = "perform piggly_branch($PIGGLY$#{node.body.tag_id}$PIGGLY$);#{node.indent(:bodySpace)}"
85
+ end
86
+ end
87
+
88
+ # Traverse children (in which we just injected code)
89
+ node.elements.map{|e| traverse(e, oid, tags) }.join
90
+ end
91
+ end
92
+ end
93
+
94
+ class << TraceCompiler
95
+
96
+ # Each of these files' mtimes are used to determine when another file is stale
97
+ def cache_sources
98
+ [Parser.grammar_path,
99
+ Parser.parser_path,
100
+ Parser.nodes_path]
101
+ end
102
+ end
103
+
104
+ end
105
+ end