piggly 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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